Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 5
552
0.00% covered (danger)
0.00%
0 / 1
 onParserFirstCallInit
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 checkOverrideParam
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 checkJosaonlyParam
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getJosa
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 convertToJohabCode
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2/**
3 * Extension:Josa
4 * Author: Bae Junehyeon <devunt@gmail.com>
5 * Original implementation by Park Shinjo <peremen@gmail.com>
6 * License: MIT License
7 */
8
9namespace MediaWiki\Extension\Josa;
10
11use MediaWiki\Hook\ParserFirstCallInitHook;
12use Parser;
13
14class Hooks implements ParserFirstCallInitHook {
15    /** @var array */
16    private static $josaMap = [
17        'Eul/Ruel' => [ '을', '를', '을(를)' ], // 곶감을 / 사과를
18        'Eun/Neun' => [ '은', '는', '은(는)' ], // 곶감은 / 사과는
19        'E/Ga' => [ '이', '가', '이(가)' ], // 곶감이 / 사과가
20        'Gwa/Wa' => [ '과', '와', '과(와)' ], // 곶감과 / 사과와
21        'A/Ya' => [ '아', '야', '아(야)' ], // 태준아 / 철수야
22        'Euro/Ro'  => [ '으로', '로', '(으)로' ], // 집으로 / 학교로
23        'E/'  => [ '이', '', '(이)' ], // 태준이가 / 철수가
24    ];
25
26    /**
27     * This dictionary matches each numbers and alphabets to the corresponding hangul pronounces.
28     *
29     * @var array
30     */
31    private static $pronounceMap = [
32        '0' => '영', '1' => '일', '2' => '이',
33        '3' => '삼', '4' => '사', '5' => '오',
34        '6' => '육', '7' => '칠', '8' => '팔',
35        '9' => '구',
36
37        'A' => '이', 'B' => '비', 'C' => '씨',
38        'D' => '디', 'E' => '이', 'F' => '프',
39        'G' => '지', 'H' => '치', 'I' => '이',
40        'J' => '이', 'K' => '이', 'L' => '엘',
41        'M' => '엠', 'N' => '엔', 'O' => '오',
42        'P' => '피', 'Q' => '큐', 'R' => '알',
43        'S' => '스', 'T' => '티', 'U' => '유',
44        'V' => '이', 'W' => '유', 'X' => '스',
45        'Y' => '이', 'Z' => '지',
46    ];
47
48    /**
49     * @param Parser $parser
50     */
51    public function onParserFirstCallInit( $parser ) {
52        foreach ( self::$josaMap as $key => $value ) {
53            $parser->setFunctionHook( $key, static function (
54                $parser, $str, $param1 = null, $param2 = null
55            ) use ( $key ) {
56                $josa = self::getJosa( $key, $str );
57
58                foreach ( [ $param1, $param2 ] as $param ) {
59                    if ( $param === null ) {
60                        break;
61                    }
62
63                    $override = self::checkOverrideParam( $param );
64                    if ( $override !== false ) {
65                        $josa = $override;
66                        continue;
67                    }
68
69                    $josaonly = self::checkJosaonlyParam( $param );
70                    if ( $josaonly === true ) {
71                        $str = '';
72                        continue;
73                    }
74                }
75
76                return $str . $josa;
77            } );
78        }
79    }
80
81    /**
82     * @param string $param String to check
83     * @return bool|string Value of override param or false if it isn't presented
84     */
85    public static function checkOverrideParam( $param ) {
86        if ( preg_match( '/^(override|덮어쓰기|오버라이드)=(.*)$/', $param, $matches ) === 0 ) {
87            return false;
88        }
89        return $matches[2];
90    }
91
92    /**
93     * @param string $param String to check
94     * @return bool Existence of josaonly param
95     */
96    public static function checkJosaonlyParam( $param ) {
97        return ( $param === 'josaonly' || $param === '조사만' );
98    }
99
100    /**
101     * @param string $type Type of the last letter in the word (see JosaHooks::$josaMap's keys)
102     * @param string $str Word to determine the josa
103     * @return string Josa
104     */
105    public static function getJosa( $type, $str ) {
106        $str = preg_replace(
107            '/[\x{0000}-\x{0027}\x{002A}-\x{002F}\x{003A}-\x{0040}\x{005B}-\x{0060}\x{007B}-\x{00FF}]/u',
108            '',
109            $str
110        );
111        $chr = mb_substr( $str, -1, 1, 'utf-8' );
112        if ( array_key_exists( $chr, self::$pronounceMap ) ) {
113            $chr = self::$pronounceMap[$chr];
114        }
115        $code = self::convertToJohabCode( $chr );
116        if ( !$code ) {
117            // Not hangul
118            $idx = 2;
119        } elseif ( ( $code - 0xAC00 ) % 28 === 0 ) {
120            // No trailing consonant
121            $idx = 1;
122        } elseif ( ( $type === 'Euro/Ro' ) && ( ( $code - 0xAC00 ) % 28 === 8 ) ) {
123            // $type is Euro/Ro and trailing consonant is rieul(ㄹ).
124            // This is an exception on Korean postposition rules.
125            $idx = 1;
126        } else {
127            // Trailing consonant exists
128            $idx = 0;
129        }
130        return self::$josaMap[$type][$idx];
131    }
132
133    /**
134     * @see https://ko.wikipedia.org/wiki/%ED%95%9C%EA%B8%80_%EC%83%81%EC%9A%A9_%EC%A1%B0%ED%95%A9%ED%98%95_%EC%9D%B8%EC%BD%94%EB%94%A9
135     *
136     * @param string $str String to convert
137     * @return int|bool Converted Johab code
138     */
139    private static function convertToJohabCode( $str ) {
140        $values = [];
141        $lookingFor = 1;
142        for ( $i = 0, $len = strlen( $str ); $i < $len; $i++ ) {
143            $thisValue = ord( $str[ $i ] );
144            if ( $thisValue < 128 ) {
145                return false;
146            } else {
147                if ( count( $values ) === 0 ) {
148                    $lookingFor = ( $thisValue < 224 ) ? 2 : 3;
149                }
150                $values[] = $thisValue;
151                if ( count( $values ) === $lookingFor ) {
152                    $number = ( $lookingFor === 3 ) ?
153                        ( ( $values[0] % 16 ) * 4096 ) + ( ( $values[1] % 64 ) * 64 ) + ( $values[2] % 64 ) :
154                        ( ( $values[0] % 32 ) * 64 ) + ( $values[1] % 64 );
155                    return $number;
156                }
157            }
158        }
159        return false;
160    }
161}