PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.21
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.21
1.10.25 1.10.24 1.10.23 1.10.22 1.10.21 1.10.20 1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 All 169 releases
woocommerce-pos / includes / Templates / Thermal / Star_Markup_Thermal_Emitter.php

Star_Markup_Thermal_Emitter.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.21, at includes/Templates/Thermal/Star_Markup_Thermal_Emitter.php

384 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Star Document Markup Thermal Emitter.
4 *
5 * Emits Star Document Markup (text/vnd.star.markup) from a thermal AST produced
6 * by Thermal_Markup_Parser — the same AST consumed by the ESC/POS and ePOS-XML
7 * emitters. Used for the Star Online (stario.online) push provider, whose job
8 * API accepts Star markup but not ESC/POS or PDF.
9 *
10 * @package WCPOS\WooCommercePOS
11 */
12
13 namespace WCPOS\WooCommercePOS\Templates\Thermal;
14
15 use WCPOS\WooCommercePOS\Templates\Barcode_Symbology;
16
17 /**
18 * Star_Markup_Thermal_Emitter class.
19 */
20 class Star_Markup_Thermal_Emitter {
21 /**
22 * Accumulated markup output.
23 *
24 * @var string
25 */
26 private string $buffer = '';
27
28 /**
29 * Paper width in character columns (for fixed-width row packing).
30 *
31 * @var int
32 */
33 private int $columns = 48;
34
35 /**
36 * Current alignment (left|center|right).
37 *
38 * @var string
39 */
40 private string $align = 'left';
41
42 /**
43 * Emit Star Document Markup from a thermal AST.
44 *
45 * @param array $ast The thermal AST root (a receipt node).
46 *
47 * @return string Star Document Markup.
48 */
49 public function emit( array $ast ): string {
50 $this->buffer = '';
51 $this->align = 'left';
52 $this->columns = isset( $ast['paper_width'] ) ? (int) $ast['paper_width'] : 48;
53
54 $children = isset( $ast['children'] ) && \is_array( $ast['children'] ) ? $ast['children'] : array();
55 $this->walk_nodes( $children );
56
57 return $this->buffer;
58 }
59
60 /**
61 * Walk a list of AST nodes.
62 *
63 * @param array $nodes AST nodes.
64 */
65 private function walk_nodes( array $nodes ): void {
66 foreach ( $nodes as $node ) {
67 if ( \is_array( $node ) ) {
68 $this->walk_node( $node );
69 }
70 }
71 }
72
73 /**
74 * Walk a single AST node.
75 *
76 * @param array $node AST node.
77 */
78 private function walk_node( array $node ): void {
79 $type = isset( $node['type'] ) ? $node['type'] : '';
80
81 switch ( $type ) {
82 case 'raw-text':
83 $this->buffer .= $this->escape( isset( $node['value'] ) ? (string) $node['value'] : '' );
84 break;
85 case 'text':
86 $this->walk_nodes( isset( $node['children'] ) && \is_array( $node['children'] ) ? $node['children'] : array() );
87 break;
88 case 'bold':
89 $this->wrap( '[bold: on]', '[bold: off]', $node );
90 break;
91 case 'underline':
92 $this->wrap( '[underline: on]', '[underline: off]', $node );
93 break;
94 case 'invert':
95 $this->wrap( '[negative: on]', '[negative: off]', $node );
96 break;
97 case 'size':
98 $this->emit_size( $node );
99 break;
100 case 'align':
101 $this->emit_align( $node );
102 break;
103 case 'row':
104 $this->emit_row( $node );
105 break;
106 case 'line':
107 $this->emit_line( $node );
108 break;
109 case 'barcode':
110 $this->emit_barcode( $node );
111 break;
112 case 'qrcode':
113 $this->emit_qrcode( $node );
114 break;
115 case 'image':
116 $this->emit_image( $node );
117 break;
118 case 'cut':
119 $this->buffer .= '[cut]';
120 break;
121 case 'feed':
122 $lines = Thermal_Bounds::clamp_int(
123 isset( $node['lines'] ) ? $node['lines'] : null,
124 Thermal_Bounds::FEED_LINES_MIN,
125 Thermal_Bounds::FEED_LINES_MIN,
126 Thermal_Bounds::FEED_LINES_MAX
127 );
128 $this->buffer .= str_repeat( '[feed]', $lines );
129 break;
130 case 'drawer':
131 $this->buffer .= '[drawer: on]';
132 break;
133 case 'receipt':
134 $this->walk_nodes( isset( $node['children'] ) && \is_array( $node['children'] ) ? $node['children'] : array() );
135 break;
136 }
137 }
138
139 /**
140 * Emit a stateful wrapping tag pair around a node's children.
141 *
142 * @param string $open Opening tag.
143 * @param string $close Closing tag.
144 * @param array $node AST node.
145 */
146 private function wrap( string $open, string $close, array $node ): void {
147 $this->buffer .= $open;
148 $this->walk_nodes( isset( $node['children'] ) && \is_array( $node['children'] ) ? $node['children'] : array() );
149 $this->buffer .= $close;
150 }
151
152 /**
153 * Emit a magnify-wrapped block, restoring 1x afterward.
154 *
155 * @param array $node Size AST node.
156 */
157 private function emit_size( array $node ): void {
158 $width = isset( $node['width'] ) ? max( 1, (int) $node['width'] ) : 1;
159 $height = isset( $node['height'] ) ? max( 1, (int) $node['height'] ) : 1;
160 $this->buffer .= sprintf( '[magnify: width %d; height %d]', $width, $height );
161 $this->walk_nodes( isset( $node['children'] ) && \is_array( $node['children'] ) ? $node['children'] : array() );
162 $this->buffer .= '[magnify]';
163 }
164
165 /**
166 * Emit an alignment-wrapped block, restoring the previous alignment.
167 *
168 * @param array $node Align AST node.
169 */
170 private function emit_align( array $node ): void {
171 $previous = $this->align;
172 $mode = isset( $node['mode'] ) ? (string) $node['mode'] : 'left';
173 $this->align = $mode;
174 $this->buffer .= '[align: ' . $this->align_value( $mode ) . ']';
175 $this->walk_nodes( isset( $node['children'] ) && \is_array( $node['children'] ) ? $node['children'] : array() );
176 $this->align = $previous;
177 $this->buffer .= '[align: ' . $this->align_value( $previous ) . ']';
178 }
179
180 /**
181 * Map an alignment mode to its Star markup value ('center' → 'middle').
182 *
183 * @param string $mode Alignment mode.
184 *
185 * @return string
186 */
187 private function align_value( string $mode ): string {
188 if ( 'center' === $mode ) {
189 return 'middle';
190 }
191 if ( 'right' === $mode ) {
192 return 'right';
193 }
194
195 return 'left';
196 }
197
198 /**
199 * Emit a row as one fixed-width line so columns align predictably.
200 *
201 * @param array $node Row AST node.
202 */
203 private function emit_row( array $node ): void {
204 $cols = isset( $node['children'] ) && \is_array( $node['children'] ) ? $node['children'] : array();
205 $widths = Thermal_Text_Layout::resolve_row_widths( $cols, $this->columns );
206
207 $line = '';
208 foreach ( $cols as $index => $col ) {
209 $width = isset( $widths[ $index ] ) ? $widths[ $index ] : 1;
210 $children = isset( $col['children'] ) && \is_array( $col['children'] ) ? $col['children'] : array();
211 $text = Thermal_Text_Layout::truncate_display( Thermal_Text_Layout::extract_text( $children ), $width );
212 $markup = $this->render_inline( $children, $width );
213 $pad = max( 0, $width - Thermal_Text_Layout::display_width( $text ) );
214 $align = isset( $col['align'] ) ? $col['align'] : 'left';
215 $line .= ( 'right' === $align )
216 ? str_repeat( ' ', $pad ) . $markup
217 : $markup . str_repeat( ' ', $pad );
218 }
219
220 $this->buffer .= '[fixedWidth: on]' . $line . '[fixedWidth: off]' . "\n";
221 }
222
223 /**
224 * Emit a horizontal rule across the paper width.
225 *
226 * @param array $node Line AST node.
227 */
228 private function emit_line( array $node ): void {
229 $style = isset( $node['style'] ) ? (string) $node['style'] : 'single';
230 $char = ( 'double' === $style ) ? '=' : '-';
231 $this->buffer .= '[fixedWidth: on]' . str_repeat( $char, $this->columns ) . '[fixedWidth: off]' . "\n";
232 }
233
234 /**
235 * Emit a 1D barcode.
236 *
237 * @param array $node Barcode AST node.
238 */
239 private function emit_barcode( array $node ): void {
240 $value = isset( $node['value'] ) ? (string) $node['value'] : '';
241 $type = isset( $node['barcode_type'] ) ? (string) $node['barcode_type'] : 'code128';
242 $this->buffer .= sprintf( '[barcode: type %s; data "%s"; hri]', Barcode_Symbology::star_markup_name( $type ), $this->data_escape( $value ) );
243 }
244
245 /**
246 * Emit a QR code.
247 *
248 * @param array $node QR AST node.
249 */
250 private function emit_qrcode( array $node ): void {
251 $value = isset( $node['value'] ) ? (string) $node['value'] : '';
252 $cell = isset( $node['size'] ) ? max( 1, min( 8, (int) $node['size'] ) ) : 3;
253 $this->buffer .= sprintf( '[barcode: type qr; data "%s"; cell %d; ec medium]', $this->data_escape( $value ), $cell );
254 }
255
256 /**
257 * Emit a logo image, only for a public http(s) URL.
258 *
259 * @param array $node Image AST node.
260 */
261 private function emit_image( array $node ): void {
262 $src = isset( $node['src'] ) ? trim( (string) $node['src'] ) : '';
263 if ( '' === $src ) {
264 return;
265 }
266
267 if ( 0 === strpos( $src, '/' ) && 0 !== strpos( $src, '//' ) ) {
268 $src = home_url( $src );
269 }
270
271 if ( ! preg_match( '#^https?://#i', $src ) ) {
272 return;
273 }
274
275 $this->buffer .= sprintf( '[image: url %s; width 100%%]', $src );
276 }
277
278 /**
279 * Escape text content for Star markup. Bracket characters are markup-significant.
280 *
281 * @param string $value Text.
282 *
283 * @return string
284 */
285 private function escape( string $value ): string {
286 return str_replace( array( '[', ']' ), array( '[[', ']]' ), $value );
287 }
288
289 /**
290 * Escape a value placed inside a quoted tag parameter (e.g. barcode data).
291 *
292 * @param string $value Value.
293 *
294 * @return string
295 */
296 private function data_escape( string $value ): string {
297 return str_replace( array( '\\', '"' ), array( '\\\\', '\\"' ), $value );
298 }
299
300 /**
301 * Render inline text nodes for fixed-width rows while preserving formatting.
302 *
303 * Star markup tags are not printed characters, so padding/truncation is based
304 * on extracted text while the emitted inline content keeps style wrappers.
305 *
306 * @param array $nodes AST nodes.
307 * @param int $width Maximum display width.
308 *
309 * @return string Star markup inline content.
310 */
311 private function render_inline( array $nodes, int $width ): string {
312 $remaining = max( 0, $width );
313
314 return $this->render_inline_nodes( $nodes, $remaining );
315 }
316
317 /**
318 * Render a list of inline nodes, consuming display width as raw text is emitted.
319 *
320 * @param array $nodes AST nodes.
321 * @param int $remaining Remaining display width.
322 *
323 * @return string Star markup inline content.
324 */
325 private function render_inline_nodes( array $nodes, int &$remaining ): string {
326 $out = '';
327 foreach ( $nodes as $node ) {
328 if ( $remaining <= 0 || ! \is_array( $node ) ) {
329 continue;
330 }
331 $out .= $this->render_inline_node( $node, $remaining );
332 }
333
334 return $out;
335 }
336
337 /**
338 * Render a single inline node for use inside a fixed-width row.
339 *
340 * @param array $node AST node.
341 * @param int $remaining Remaining display width.
342 *
343 * @return string Star markup inline content.
344 */
345 private function render_inline_node( array $node, int &$remaining ): string {
346 $type = isset( $node['type'] ) ? (string) $node['type'] : '';
347
348 if ( 'raw-text' === $type ) {
349 $text = Thermal_Text_Layout::truncate_display( isset( $node['value'] ) ? (string) $node['value'] : '', $remaining );
350 $remaining -= Thermal_Text_Layout::display_width( $text );
351
352 return $this->escape( $text );
353 }
354
355 $children = isset( $node['children'] ) && \is_array( $node['children'] ) ? $node['children'] : array();
356 if ( 'text' === $type || 'column' === $type ) {
357 return $this->render_inline_nodes( $children, $remaining );
358 }
359
360 $wrapped = $this->render_inline_nodes( $children, $remaining );
361 if ( '' === $wrapped ) {
362 return '';
363 }
364
365 if ( 'bold' === $type ) {
366 return '[bold: on]' . $wrapped . '[bold: off]';
367 }
368 if ( 'underline' === $type ) {
369 return '[underline: on]' . $wrapped . '[underline: off]';
370 }
371 if ( 'invert' === $type ) {
372 return '[negative: on]' . $wrapped . '[negative: off]';
373 }
374 if ( 'size' === $type ) {
375 $width = isset( $node['width'] ) ? max( 1, (int) $node['width'] ) : 1;
376 $height = isset( $node['height'] ) ? max( 1, (int) $node['height'] ) : 1;
377
378 return sprintf( '[magnify: width %d; height %d]', $width, $height ) . $wrapped . '[magnify]';
379 }
380
381 return $wrapped;
382 }
383 }
384