Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 170
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikiHiero
0.00% covered (danger)
0.00%
0 / 170
0.00% covered (danger)
0.00%
0 / 16
3540
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 loadData
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 parserHook
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getScale
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setScale
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 renderGlyph
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 renderGlyphImage
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 renderVoidBlock
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getImageUrl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isMirrored
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 extractCode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 resizeGlyph
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 render
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 1
756
 getFiles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getImagePath
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * WikiHiero - A PHP convert from text using "Manual for the encoding of
4 * hieroglyphic texts for computer input" syntax to HTML entities (table and
5 * images).
6 *
7 * Copyright (C) 2004 Guillaume Blanchard (Aoineko)
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 * http://www.gnu.org/copyleft/gpl.html
23 */
24
25namespace WikiHiero;
26
27use MediaWiki\Config\Config;
28use MediaWiki\Context\RequestContext;
29use MediaWiki\Html\Html;
30use MediaWiki\Output\OutputPage;
31use MediaWiki\Parser\Parser;
32
33class WikiHiero {
34    public const IMAGE_EXT = 'png';
35    private const IMAGE_PREFIX = 'hiero_';
36
37    /** Use default scale */
38    private const DEFAULT_SCALE = -1;
39    private const CARTOUCHE_WIDTH = 2;
40    private const IMAGE_MARGIN = 1;
41    private const MAX_HEIGHT = 44;
42
43    private const TABLE_START = '<table class="mw-hiero-table">';
44
45    private $scale = 100;
46    private $config;
47
48    /** @var string[] */
49    private static $phonemes;
50    /** @var string[] */
51    private static $prefabs;
52    /** @var int[][] */
53    private static $files;
54
55    /**
56     * @param Config|null $config
57     */
58    public function __construct( Config $config = null ) {
59        $this->config = $config ?: RequestContext::getMain()->getConfig();
60        self::loadData();
61    }
62
63    /**
64     * Loads hieroglyph information
65     * @suppress PhanUndeclaredVariable,PhanTypeMismatchPropertyProbablyReal Phan doesn't understand require_once
66     */
67    private static function loadData() {
68        if ( self::$phonemes ) {
69            return;
70        }
71        require_once dirname( __DIR__ ) . '/data/tables.php';
72        self::$phonemes = $wh_phonemes;
73        self::$prefabs = $wh_prefabs;
74        self::$files = $wh_files;
75    }
76
77    /**
78     * Parser callback for <hiero> tag
79     * @param string $input
80     * @param array $args
81     * @param Parser $parser
82     * @return string
83     */
84    public static function parserHook( $input, $args, $parser ) {
85        $hiero = new WikiHiero();
86        $parser->getOutput()->addModuleStyles( [ 'ext.wikihiero' ] );
87        $parser->addTrackingCategory( 'wikihiero-usage-tracking-category' );
88        // Strip newlines to avoid breakage in the wiki parser block pass
89        return str_replace( "\n", " ", $hiero->render( $input ) );
90    }
91
92    /**
93     * @return int Scale of overall hieroglyphic block in percents
94     */
95    public function getScale() {
96        return $this->scale;
97    }
98
99    /**
100     * Sets the scale of overall hieroglyphic block in percents
101     *
102     * @param int $scale
103     */
104    public function setScale( $scale ) {
105        $this->scale = $scale;
106    }
107
108    /**
109     * Renders a glyph
110     *
111     * @param string $glyph glyph's code to render
112     * @param int|null $height glyph size in pixels or null to omit
113     * @return string a string to add to the stream
114     */
115    private function renderGlyph( $glyph, $height = null ) {
116        // Support skins with night theme.
117        $imageClass = 'skin-invert';
118        if ( $this->isMirrored( $glyph ) ) {
119            $imageClass .= ' mw-mirrored';
120        }
121        $glyph = $this->extractCode( $glyph );
122
123        if ( $glyph == '..' ) {
124            // Render void block
125            return $this->renderVoidBlock( self::MAX_HEIGHT );
126        }
127        if ( $glyph == '.' ) {
128            // Render half-width void block
129            return $this->renderVoidBlock( self::MAX_HEIGHT / 2 );
130        }
131
132        if ( $glyph == '<' || $glyph == '>' ) {
133            // Render cartouches
134            return $this->renderGlyphImage( $glyph, self::MAX_HEIGHT, null, $imageClass );
135        }
136
137        return $this->renderGlyphImage( $glyph, $height, self::IMAGE_MARGIN, $imageClass );
138    }
139
140    /**
141     * Renders a glyph into an <img> tag
142     *
143     * @param string $glyph Glyph to render
144     * @param int|null $height Image height, if null don't set explicitly
145     * @param int|null $margin Margin, if null don't set
146     * @param string|null $class Class for <img> tag
147     * @return string Rendered HTML
148     */
149    private function renderGlyphImage( $glyph, $height = null, $margin = null, $class = null ) {
150        if ( array_key_exists( $glyph, self::$phonemes ) ) {
151            $code = self::$phonemes[$glyph];
152            $fileName = $code;
153            // Don't show image name for cartouches and such
154            $title = preg_match( '/^[A-Za-z0-9]+$/', $glyph ) ? "{$code} [{$glyph}]" : $glyph;
155        } else {
156            $fileName = $title = $glyph;
157        }
158        if ( !array_key_exists( $fileName, self::$files ) ) {
159            return htmlspecialchars( $glyph );
160        }
161
162        $style = $margin === null ? null : "margin: {$margin}px;";
163        $attribs = [
164            'class' => $class,
165            'style' => $style,
166            'src' => $this->getImageUrl( $fileName ),
167            'height' => $height,
168            'title' => $title,
169            'alt' => $glyph,
170        ];
171        return Html::element( 'img', $attribs );
172    }
173
174    /**
175     * Returns HTML for a void block
176     * @param int $width
177     * @return string
178     */
179    private function renderVoidBlock( $width ) {
180        $width = intval( $width );
181        return Html::rawElement(
182            'table',
183            [
184                'class' => 'mw-hiero-table',
185                'style' => "width: {$width}px;",
186            ],
187            '<tr><td>&#160;</td></tr>'
188        );
189    }
190
191    private function getImageUrl( $fileName ) {
192        $url = self::getImagePath() . self::IMAGE_PREFIX . $fileName . '.' . self::IMAGE_EXT;
193        return OutputPage::transformResourcePath( $this->config, $url );
194    }
195
196    private function isMirrored( $glyph ) {
197        return substr( $glyph, -1 ) == '\\';
198    }
199
200    /**
201     * Extracts hieroglyph code from glyph, e.g. A1\ --> A1
202     *
203     * @param string $glyph
204     * @return string
205     */
206    private function extractCode( $glyph ) {
207        return preg_replace( '/\\\\.*$/', '', $glyph );
208    }
209
210    /**
211     * Resize a glyph
212     *
213     * @param string $item glyph code
214     * @param bool $is_cartouche true if glyph is inside a cartouche
215     * @param int $total total size of a group for multi-glyph block
216     * @return int size
217     */
218    private function resizeGlyph( $item, $is_cartouche = false, $total = 0 ) {
219        $item = $this->extractCode( $item );
220        if ( array_key_exists( $item, self::$phonemes ) ) {
221            $glyph = self::$phonemes[$item];
222        } else {
223            $glyph = $item;
224        }
225
226        $margin = 2 * self::IMAGE_MARGIN;
227        if ( $is_cartouche ) {
228            $margin += 2 * self::CARTOUCHE_WIDTH;
229        }
230
231        if ( array_key_exists( $glyph, self::$files ) ) {
232            $height = $margin + self::$files[$glyph][1];
233            if ( $total ) {
234                if ( $total > self::MAX_HEIGHT ) {
235                    return intval( $height * self::MAX_HEIGHT / $total ) - $margin;
236                } else {
237                    return $height - $margin;
238                }
239            } else {
240                if ( $height > self::MAX_HEIGHT ) {
241                    return intval( self::MAX_HEIGHT * self::MAX_HEIGHT / $height ) - $margin;
242                } else {
243                    return $height - $margin;
244                }
245            }
246        }
247
248        return self::MAX_HEIGHT - $margin;
249    }
250
251    /**
252     * Render hieroglyph text
253     *
254     * @param string $hiero text to convert
255     * @param int $scale global scale in percentage (default = 100%)
256     * @param bool $line use line (default = false)
257     * @return string converted code
258     */
259    public function render( $hiero, $scale = self::DEFAULT_SCALE, $line = false ) {
260        if ( $scale != self::DEFAULT_SCALE ) {
261            $this->setScale( $scale );
262        }
263
264        $html = "";
265
266        if ( $line ) {
267            $html .= "<hr />\n";
268        }
269
270        $tokenizer = new HieroTokenizer( $hiero );
271        $blocks = $tokenizer->tokenize();
272        $contentHtml = $tableHtml = $tableContentHtml = "";
273        $is_cartouche = false;
274
275        // ------------------------------------------------------------------------
276        // Loop into all blocks
277        foreach ( $blocks as $code ) {
278            // simplest case, the block contain only 1 code -> render
279            if ( count( $code ) == 1 ) {
280                if ( $code[0] == '!' ) {
281                    // end of line
282                    $tableHtml = '</tr></table>' . self::TABLE_START . "<tr>\n";
283                    if ( $line ) {
284                        $contentHtml .= "<hr />\n";
285                    }
286
287                } elseif ( strstr( $code[0], '<' ) ) {
288                    // start cartouche
289                    $contentHtml .= '<td>' . $this->renderGlyph( $code[0] ) . '</td>';
290                    $is_cartouche = true;
291                    $contentHtml .= '<td>' .
292                        self::TABLE_START . "<tr><td class=\"mw-hiero-box\" style=\"height: " .
293                        self::CARTOUCHE_WIDTH . "px;\"></td></tr><tr><td>" . self::TABLE_START .
294                        "<tr>";
295
296                } elseif ( strstr( $code[0], '>' ) ) {
297                    // end cartouche
298                    $contentHtml .= "</tr></table></td></tr><tr><td class=\"mw-hiero-box\" " .
299                        "style=\"height: " . self::CARTOUCHE_WIDTH .
300                        'px;"></td></tr></table></td>';
301                    $is_cartouche = false;
302                    $contentHtml .= '<td>' . $this->renderGlyph( $code[0] ) . '</td>';
303
304                } elseif ( $code[0] != "" ) {
305                    // assume it's a glyph or '..' or '.'
306                    $contentHtml .= '<td>' . $this->renderGlyph(
307                        $code[0],
308                        $this->resizeGlyph( $code[0], $is_cartouche )
309                    ) . '</td>';
310                }
311
312            // block contains more than 1 glyph
313            } else {
314                // convert all codes into '&' to test prefabs glyph
315                $prefabs = "";
316                foreach ( $code as $t ) {
317                    if ( preg_match( "/[*:!()]/", $t[0] ) ) {
318                        $prefabs .= "&";
319                    } else {
320                        $prefabs .= $t;
321                    }
322                }
323
324                // test if block exists in the prefabs list
325                if ( in_array( $prefabs, self::$prefabs, true ) ) {
326                    $contentHtml .= '<td>' . $this->renderGlyph(
327                        $prefabs,
328                        $this->resizeGlyph( $prefabs, $is_cartouche )
329                    ) . '</td>';
330
331                // block must be manually computed
332                } else {
333                    // get block total height
334                    $line_max = 0;
335                    $total    = 0;
336                    $height   = 0;
337
338                    foreach ( $code as $t ) {
339                        if ( $t == ":" ) {
340                            if ( $height > $line_max ) {
341                                $line_max = $height;
342                            }
343                            $total += $line_max;
344                            $line_max = 0;
345
346                        } elseif ( $t == "*" ) {
347                            if ( $height > $line_max ) {
348                                $line_max = $height;
349                            }
350                        } else {
351                            if ( array_key_exists( $t, self::$phonemes ) ) {
352                                $glyph = self::$phonemes[$t];
353                            } else {
354                                $glyph = $t;
355                            }
356                            if ( array_key_exists( $glyph, self::$files ) ) {
357                                $height = 2 + self::$files[$glyph][1];
358                            }
359                        }
360                    }
361
362                    if ( $height > $line_max ) {
363                        $line_max = $height;
364                    }
365
366                    $total += $line_max;
367
368                    // render all glyph into the block
369                    $block = "";
370                    foreach ( $code as $t ) {
371                        if ( $t == ":" ) {
372                            $block .= "<br />";
373
374                        } elseif ( $t == "*" ) {
375                            $block .= " ";
376
377                        } else {
378                            // resize the glyph according to the block total height
379                            $block .= $this->renderGlyph(
380                                $t,
381                                $this->resizeGlyph( $t, $is_cartouche, $total )
382                            );
383                        }
384                    }
385
386                    $contentHtml .= '<td>' . $block . '</td>';
387                }
388                $contentHtml .= "\n";
389            }
390
391            if ( strlen( $contentHtml ) > 0 ) {
392                $tableContentHtml .= $tableHtml . $contentHtml;
393                $contentHtml = $tableHtml = "";
394            }
395        }
396
397        if ( strlen( $tableContentHtml ) > 0 ) {
398            $html .= self::TABLE_START . "<tr>\n" . $tableContentHtml . '</tr></table>';
399        }
400
401        $style = null;
402        if ( $this->scale != 100 ) {
403            $ratio = floatval( $this->scale ) / 100;
404            $style = "transform: scale($ratio,$ratio);";
405        }
406
407        return Html::rawElement(
408            'table',
409            [
410                'class' => 'mw-hiero-table mw-hiero-outer',
411                'dir' => 'ltr',
412                'style' => $style,
413            ],
414            "<tr><td>\n$html\n</td></tr>"
415        );
416    }
417
418    /**
419     * Returns a list of image files used by this extension
420     *
421     * @return array list of files in format 'file' => [ width, height ]
422     */
423    public function getFiles() {
424        return self::$files;
425    }
426
427    /**
428     * @return string URL of images directory
429     */
430    public static function getImagePath() {
431        global $wgExtensionAssetsPath;
432        return "$wgExtensionAssetsPath/wikihiero/img/";
433    }
434
435    /**
436     * Get glyph code from file name
437     *
438     * @param string $file file name
439     * @return string converted code
440     */
441    public static function getCode( $file ) {
442        return substr( $file, strlen( self::IMAGE_PREFIX ), -( 1 + strlen( self::IMAGE_EXT ) ) );
443    }
444}