ja-compromise 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +367 -6
  3. package/builds/ja-compromise.cjs +4389 -2286
  4. package/builds/ja-compromise.min.js +1 -1
  5. package/builds/ja-compromise.mjs +1 -1
  6. package/package.json +36 -23
  7. package/src/01-one/conjugate/conjugate-adj.js +53 -0
  8. package/src/01-one/conjugate/conjugate-verb.js +145 -0
  9. package/src/01-one/conjugate/deconjugate.js +178 -0
  10. package/src/01-one/conjugate/index.js +13 -0
  11. package/src/01-one/conjugate/kana.js +38 -0
  12. package/src/01-one/conjugate/tags.js +85 -0
  13. package/src/01-one/conjugate/verb-class.js +105 -0
  14. package/src/01-one/lexicon/_data.js +30 -0
  15. package/src/01-one/lexicon/api.js +61 -0
  16. package/src/01-one/lexicon/lexicon.js +163 -0
  17. package/src/01-one/lexicon/misc.js +115 -0
  18. package/src/01-one/lexicon/plugin.js +20 -0
  19. package/src/01-one/numbers/api.js +202 -0
  20. package/src/01-one/numbers/kanji-number.js +69 -0
  21. package/src/01-one/numbers/to-kanji.js +66 -0
  22. package/src/01-one/output/compute/dict.js +13 -0
  23. package/src/01-one/output/compute/english.js +13 -0
  24. package/src/01-one/output/compute/root.js +30 -0
  25. package/src/01-one/output/debug/_color.js +16 -0
  26. package/src/01-one/output/debug/index.js +24 -0
  27. package/src/01-one/output/debug/tags.js +56 -0
  28. package/src/01-one/output/plugin.js +13 -0
  29. package/src/01-one/romanji/api.js +16 -0
  30. package/src/01-one/romanji/compute/index.js +36 -0
  31. package/src/01-one/romanji/compute/kanji-reading/index.js +91 -0
  32. package/src/01-one/romanji/compute/kanji-reading/readings.js +861 -0
  33. package/src/01-one/romanji/compute/kanji-reading/words.js +31 -0
  34. package/src/01-one/romanji/compute/toRomanji/hiragana-map.js +175 -0
  35. package/src/01-one/romanji/compute/toRomanji/index.js +84 -0
  36. package/src/01-one/romanji/compute/toRomanji/katakana-map.js +128 -0
  37. package/src/01-one/romanji/plugin.js +7 -0
  38. package/src/01-one/tokenizer/methods/join-numbers.js +32 -0
  39. package/src/01-one/tokenizer/methods/join-up.js +66 -0
  40. package/src/01-one/tokenizer/methods/lib.js +66 -0
  41. package/src/01-one/tokenizer/methods/naiive-split.js +88 -0
  42. package/src/01-one/tokenizer/methods/okurigana.js +58 -0
  43. package/src/01-one/tokenizer/methods/terms.js +143 -0
  44. package/src/01-one/tokenizer/methods/trie/build.js +13 -0
  45. package/src/01-one/tokenizer/methods/trie/split-up.js +29 -0
  46. package/src/01-one/tokenizer/methods/whitespace.js +44 -0
  47. package/src/01-one/tokenizer/plugin.js +13 -0
  48. package/src/02-two/preTagger/compute/01-script.js +44 -0
  49. package/src/02-two/preTagger/compute/02-particles.js +82 -0
  50. package/src/02-two/preTagger/compute/03-verbs.js +111 -0
  51. package/src/02-two/preTagger/compute/04-adjectives.js +43 -0
  52. package/src/02-two/preTagger/compute/05-people.js +22 -0
  53. package/src/02-two/preTagger/compute/06-numbers.js +80 -0
  54. package/src/02-two/preTagger/compute/07-dates.js +105 -0
  55. package/src/02-two/preTagger/compute/index.js +52 -0
  56. package/src/02-two/preTagger/plugin.js +9 -0
  57. package/src/02-two/tagset/plugin.js +12 -0
  58. package/src/02-two/tagset/tags/dates.js +58 -0
  59. package/src/02-two/tagset/tags/misc.js +91 -0
  60. package/src/02-two/tagset/tags/nouns.js +131 -0
  61. package/src/02-two/tagset/tags/particles.js +50 -0
  62. package/src/02-two/tagset/tags/values.js +68 -0
  63. package/src/02-two/tagset/tags/verbs.js +93 -0
  64. package/src/_lib.js +2 -0
  65. package/src/_version.js +1 -0
  66. package/src/index.js +82 -0
  67. package/types/index.d.ts +268 -0
  68. package/types/japanese.d.ts +159 -0
  69. package/types/misc.d.ts +92 -0
@@ -0,0 +1,58 @@
1
+ import lexicon from '../../lexicon/lexicon.js'
2
+ import deconjugate from '../../conjugate/deconjugate.js'
3
+ import { isKanji, isHiragana } from './lib.js'
4
+
5
+ // a particle can't be the first kana of a verb's okurigana - 「木で作った」
6
+ // is 木 + で + 作った, not 木で + 作った
7
+ const particleStart = new Set(['は', 'が', 'を', 'に', 'へ', 'と', 'も', 'の', 'か', 'ね', 'よ', 'や', 'で', 'ば'])
8
+ const MAX_TAIL = 6
9
+
10
+ const isHiraganaWord = (str) => str.split('').every(isHiragana)
11
+
12
+ /**
13
+ * 含まれている is a verb even though 含む isn't in the lexicon.
14
+ * glue an unknown kanji stem onto the inflectional tail that follows it.
15
+ */
16
+ const attachOkurigana = function (arr) {
17
+ let out = []
18
+ for (let i = 0; i < arr.length; i += 1) {
19
+ let head = arr[i]
20
+ // only an unknown kanji chunk can pick up okurigana
21
+ if (lexicon[head] || !head.split('').every(isKanji)) {
22
+ out.push(head)
23
+ continue
24
+ }
25
+ // collect the hiragana tokens that follow it
26
+ let tail = ''
27
+ let end = i
28
+ for (let o = i + 1; o < arr.length; o += 1) {
29
+ if (!isHiraganaWord(arr[o]) || tail.length + arr[o].length > MAX_TAIL) {
30
+ break
31
+ }
32
+ tail += arr[o]
33
+ end = o
34
+ }
35
+ if (tail === '' || particleStart.has(tail[0])) {
36
+ out.push(head)
37
+ continue
38
+ }
39
+ // try the longest tail first, shrinking a token at a time
40
+ let joined = head
41
+ let taken = i
42
+ for (let o = end; o > i; o -= 1) {
43
+ let candidate = head + arr.slice(i + 1, o + 1).join('')
44
+ if (candidate.length - head.length < 2) {
45
+ continue
46
+ }
47
+ if (lexicon[candidate] || deconjugate(candidate)) {
48
+ joined = candidate
49
+ taken = o
50
+ break
51
+ }
52
+ }
53
+ out.push(joined)
54
+ i = taken
55
+ }
56
+ return out
57
+ }
58
+ export default attachOkurigana
@@ -0,0 +1,143 @@
1
+ import buildIndex from './trie/build.js'
2
+ import lexicon from '../../lexicon/lexicon.js'
3
+ import splitUp from './trie/split-up.js'
4
+ import joinUp from './join-up.js'
5
+ import attachOkurigana from './okurigana.js'
6
+ import joinNumbers from './join-numbers.js'
7
+ import { isPunctuation } from './lib.js'
8
+
9
+ const { words, maxLen } = buildIndex(Object.keys(lexicon))
10
+ // a very long 'word' is nearly always two words - cap the lookahead
11
+ const LOOKAHEAD = Math.min(maxLen, 12)
12
+
13
+ const allHiragana = /^[぀-ゟ]+$/
14
+
15
+ // a case-particle at either end of a kana run isn't part of the word
16
+ const edgeParticle = new Set(['は', 'が', 'を', 'に', 'へ', 'と', 'も', 'の', 'で', 'や', 'か'])
17
+ // beyond this, a string of loose kana really is several words
18
+ const MAX_KANA_RUN = 7
19
+
20
+ const isSingleKana = (str) => str.length === 1 && allHiragana.test(str)
21
+
22
+ // 「『((【〔《〈 at the end of a punctuation run
23
+ const trailingOpen = /[「『((【〔《〈]+$/
24
+
25
+ /**
26
+ * ひらがな comes back from the matcher as ひ|ら|が|な, because が and な happen
27
+ * to be particles. a stretch of single kana that matched nothing is one
28
+ * unknown word - a name, or a word the lexicon is missing.
29
+ */
30
+ const rejoinKana = function (arr) {
31
+ let out = []
32
+ for (let i = 0; i < arr.length; i += 1) {
33
+ if (!isSingleKana(arr[i])) {
34
+ out.push(arr[i])
35
+ continue
36
+ }
37
+ // how far does this stretch of loose kana go?
38
+ let end = i
39
+ while (end < arr.length && isSingleKana(arr[end])) {
40
+ end += 1
41
+ }
42
+ let run = arr.slice(i, end)
43
+ i = end - 1
44
+ // を is only ever a particle, so it's a hard word-boundary
45
+ let segments = [[]]
46
+ run.forEach(c => {
47
+ if (c === 'を') {
48
+ segments.push(['を'], [])
49
+ } else {
50
+ segments[segments.length - 1].push(c)
51
+ }
52
+ })
53
+ segments.forEach(seg => {
54
+ if (seg.length === 0) {
55
+ return
56
+ }
57
+ if (seg.length === 1 || seg[0] === 'を' || seg.length > MAX_KANA_RUN) {
58
+ out = out.concat(seg)
59
+ return
60
+ }
61
+ // 「での」 is two particles in a row, not a word
62
+ if (seg.every(c => edgeParticle.has(c))) {
63
+ out = out.concat(seg)
64
+ return
65
+ }
66
+ // 「ひらがなと」 is ひらがな + と, 「経験につながる」 is 経験 | に | つながる.
67
+ // a particle can't begin a clause, so only trim the head when something
68
+ // real came before it
69
+ let hasLeft = out.length > 0 && !edgeParticle.has(out[out.length - 1])
70
+ let head = ''
71
+ let tail = ''
72
+ if (seg.length > 2 && hasLeft && edgeParticle.has(seg[0])) {
73
+ head = seg.shift()
74
+ }
75
+ if (seg.length > 2 && edgeParticle.has(seg[seg.length - 1])) {
76
+ tail = seg.pop()
77
+ }
78
+ if (head) {
79
+ out.push(head)
80
+ }
81
+ out.push(seg.join(''))
82
+ if (tail) {
83
+ out.push(tail)
84
+ }
85
+ })
86
+ }
87
+ return out
88
+ }
89
+
90
+ const tokenize = function (txt, isChunk) {
91
+ // when the writer used a space, that chunk is one word - beginner japanese
92
+ // is often written 「たろう は のりこ を…」, and 「のりこ」 must not be
93
+ // greedily read as the particle の plus りこ
94
+ if (isChunk === true && txt.length <= 6 && allHiragana.test(txt) && !lexicon[txt]) {
95
+ return [txt]
96
+ }
97
+ // split by known-word segments
98
+ let arr = splitUp(txt, words, LOOKAHEAD)
99
+ // a kana run that fell apart into loose characters is one unknown word
100
+ arr = rejoinKana(arr)
101
+ // join-up neighbouring unknown characters
102
+ arr = joinUp(arr)
103
+ // 二|十|三 is one number
104
+ arr = joinNumbers(arr)
105
+ // give an unknown kanji stem its inflectional tail - 含 + まれている
106
+ arr = attachOkurigana(arr)
107
+ // punctuation is never its own term - it hangs off the word before it,
108
+ // except an opening bracket, which hangs off the word after it - 彼は「はい」と
109
+ let out = []
110
+ let carry = ''
111
+ arr.forEach(str => {
112
+ if (str === '') {
113
+ return
114
+ }
115
+ if (str.split('').every(isPunctuation)) {
116
+ let open = ''
117
+ str = str.replace(trailingOpen, m => {
118
+ open = m
119
+ return ''
120
+ })
121
+ if (str !== '' && out.length > 0) {
122
+ out[out.length - 1] += str
123
+ } else {
124
+ carry += str
125
+ }
126
+ carry += open
127
+ return
128
+ }
129
+ out.push(carry + str)
130
+ carry = ''
131
+ })
132
+ // punctuation with no word after it
133
+ if (carry !== '') {
134
+ if (out.length > 0) {
135
+ out[out.length - 1] += carry
136
+ } else {
137
+ out.push(carry)
138
+ }
139
+ }
140
+ return out
141
+ }
142
+ export default tokenize
143
+ export { words }
@@ -0,0 +1,13 @@
1
+ // index the lexicon for longest-match lookup
2
+ const buildIndex = function (arr) {
3
+ let words = new Set()
4
+ let maxLen = 1
5
+ for (let i = 0; i < arr.length; i += 1) {
6
+ words.add(arr[i])
7
+ if (arr[i].length > maxLen) {
8
+ maxLen = arr[i].length
9
+ }
10
+ }
11
+ return { words, maxLen }
12
+ }
13
+ export default buildIndex
@@ -0,0 +1,29 @@
1
+ // longest-match segmentation.
2
+ //
3
+ // this used to build a character-trie over the whole lexicon, which cost
4
+ // ~40mb of nodes. a Set of the words plus a bounded backwards scan is the
5
+ // same answer for a fraction of the memory.
6
+
7
+ const splitUp = function (txt, words, maxLen) {
8
+ let out = []
9
+ let i = 0
10
+ while (i < txt.length) {
11
+ let max = Math.min(maxLen, txt.length - i)
12
+ let found = ''
13
+ for (let len = max; len > 1; len -= 1) {
14
+ let str = txt.substr(i, len)
15
+ if (words.has(str)) {
16
+ found = str
17
+ break
18
+ }
19
+ }
20
+ if (found === '') {
21
+ // a single character is only a 'word' if the lexicon says so
22
+ found = txt[i]
23
+ }
24
+ out.push(found)
25
+ i += found.length
26
+ }
27
+ return out
28
+ }
29
+ export default splitUp
@@ -0,0 +1,44 @@
1
+ import toTerms from './terms.js'
2
+ const before = /^[\s「『((〽【〔《〈]+/
3
+ const after = /[\s、。,.:;・」』))…〜~】〕》〉\.?!?!]+$/
4
+
5
+ const getPunct = function (str) {
6
+ let pre = ''
7
+ let post = ''
8
+ let inside = str
9
+ // strip-off leading punctuation
10
+ inside = inside.replace(before, m => {
11
+ pre = m || ''
12
+ return ''
13
+ })
14
+ // strip-off ending punctuation
15
+ inside = inside.replace(after, m => {
16
+ post = m || ''
17
+ return ''
18
+ })
19
+ return { pre, inside, post }
20
+ }
21
+
22
+ const tokenize = function (txt, isChunk) {
23
+ let terms = []
24
+ toTerms(txt, isChunk).forEach(str => {
25
+ let { pre, post, inside } = getPunct(str)
26
+ if (inside === '') {
27
+ // punctuation-only - fold it into the term before it
28
+ if (terms.length > 0) {
29
+ terms[terms.length - 1].post += pre + post
30
+ return
31
+ }
32
+ return
33
+ }
34
+ terms.push({
35
+ text: inside,
36
+ normal: inside.toLowerCase(),
37
+ pre,
38
+ post,
39
+ tags: new Set(),
40
+ })
41
+ })
42
+ return terms
43
+ }
44
+ export default tokenize
@@ -0,0 +1,13 @@
1
+ // import unicode from './unicode.js'
2
+ // import contractions from './contractions.js'
3
+ import toTerms from './methods/terms.js'
4
+
5
+ export default {
6
+ // mutate: (world) => {
7
+ // world.model.one.unicode = unicode
8
+ // world.model.one.contractions = contractions
9
+ // },
10
+ methods: {
11
+ toTerms: toTerms
12
+ }
13
+ }
@@ -0,0 +1,44 @@
1
+ import { isHiragana, isKatakana, isKanji, isAscii, isNumber } from '../../../01-one/tokenizer/methods/lib.js'
2
+
3
+ const every = function (str, fn) {
4
+ for (let i = 0; i < str.length; i += 1) {
5
+ if (!fn(str[i])) {
6
+ return false
7
+ }
8
+ }
9
+ return str.length > 0
10
+ }
11
+
12
+ const tagScript = function (terms, setTag, world) {
13
+ const reason = 'script'
14
+ terms.forEach(term => {
15
+ let str = term.text
16
+ if (every(str, isHiragana)) {
17
+ setTag([term], 'Hiragana', world, null, reason)
18
+ return
19
+ }
20
+ if (every(str, c => isKatakana(c) || c === 'ー')) {
21
+ setTag([term], 'Katakana', world, null, reason)
22
+ // a katakana word is nearly always a loanword noun
23
+ if (term.tags.size <= 1) {
24
+ setTag([term], 'Noun', world, null, reason)
25
+ }
26
+ return
27
+ }
28
+ if (every(str, isKanji)) {
29
+ setTag([term], 'Kanji', world, null, reason)
30
+ return
31
+ }
32
+ if (every(str, isNumber)) {
33
+ setTag([term], 'Value', world, null, reason)
34
+ setTag([term], 'Cardinal', world, null, reason)
35
+ return
36
+ }
37
+ if (every(str, isAscii)) {
38
+ setTag([term], 'Ascii', world, null, reason)
39
+ return
40
+ }
41
+ // a mixed kanji+hiragana word (書いた, 食べる) - no single script tag
42
+ })
43
+ }
44
+ export default tagScript
@@ -0,0 +1,82 @@
1
+ // what a particle tells us about the word in front of it.
2
+ // this is the workhorse of japanese pos-tagging: the particle *is* the syntax.
3
+ const reason = 'particle'
4
+
5
+ // particles that only ever follow a noun-phrase
6
+ const nounBefore = {
7
+ 'は': 'Topic',
8
+ 'が': 'Noun',
9
+ 'を': 'Object',
10
+ 'へ': 'Noun',
11
+ 'から': 'Noun',
12
+ 'まで': 'Noun',
13
+ 'より': 'Noun',
14
+ 'の': 'Possessive',
15
+ }
16
+ // に and で follow nouns, but also verb-stems (見に行く) and な-adjectives (静かで)
17
+ const softNounBefore = new Set(['に', 'で', 'と', 'も'])
18
+
19
+ // particles that only ever follow a verb or adjective
20
+ const verbBefore = new Set(['て', 'ば', 'たら', 'ながら', 'ので', 'のに', 'けれど', 'けれども', 'けど', 'たり'])
21
+
22
+ const tagParticles = function (terms, setTag, world) {
23
+ for (let i = 0; i < terms.length; i += 1) {
24
+ let t = terms[i]
25
+ let prev = terms[i - 1]
26
+ let next = terms[i + 1]
27
+ let str = t.text
28
+
29
+ // な before a noun is the adnominal copula (静かな人), not a final particle
30
+ if (str === 'な' && next && !next.tags.has('Verb')) {
31
+ setTag([t], 'AdnominalParticle', world, null, reason)
32
+ if (prev) {
33
+ setTag([prev], 'NaAdjective', world, null, reason)
34
+ }
35
+ continue
36
+ }
37
+ // か at the end of a clause is a question-marker, not 'or'
38
+ if (str === 'か' && (!next || next.tags.has('SentenceParticle'))) {
39
+ setTag([t], 'SentenceParticle', world, null, reason)
40
+ setTag([t], 'QuestionWord', world, null, reason)
41
+ continue
42
+ }
43
+ // と after a verb introduces a quote; between two nouns it means 'and'
44
+ if (str === 'と' && prev) {
45
+ if (prev.tags.has('Verb') || prev.tags.has('Copula')) {
46
+ setTag([t], 'QuotativeParticle', world, null, reason)
47
+ continue
48
+ }
49
+ if (next && !next.tags.has('Verb')) {
50
+ setTag([t], 'Conjunction', world, null, reason)
51
+ }
52
+ }
53
+ // で after a noun is 'at/by'; after a な-adjective it's the copula
54
+ if (str === 'で' && prev && prev.tags.has('NaAdjective')) {
55
+ setTag([t], 'Copula', world, null, reason)
56
+ setTag([t], 'Gerund', world, null, reason)
57
+ continue
58
+ }
59
+
60
+ // a particle never marks another particle
61
+ if (!prev || prev.tags.has('Particle')) {
62
+ continue
63
+ }
64
+ if (nounBefore[str] !== undefined) {
65
+ if (!prev.tags.has('Verb') && !prev.tags.has('Adjective')) {
66
+ setTag([prev], nounBefore[str], world, null, reason)
67
+ } else if (str === 'の' || str === 'が') {
68
+ // 走るの / 行くが - the verb is nominalized or the clause continues
69
+ setTag([t], str === 'の' ? 'AdnominalParticle' : 'ConjunctiveParticle', world, null, reason)
70
+ }
71
+ continue
72
+ }
73
+ if (softNounBefore.has(str) && !prev.tags.has('Verb') && !prev.tags.has('Adjective') && prev.tags.size <= 1) {
74
+ setTag([prev], 'Noun', world, null, reason)
75
+ continue
76
+ }
77
+ if (verbBefore.has(str) && prev.tags.size <= 1) {
78
+ setTag([prev], 'Verb', world, null, reason)
79
+ }
80
+ }
81
+ }
82
+ export default tagParticles
@@ -0,0 +1,111 @@
1
+ import { deconjugate, conjugateVerb } from '../../../01-one/conjugate/index.js'
2
+ import lexicon from '../../../01-one/lexicon/lexicon.js'
3
+
4
+ const reason = 'verbSuffix'
5
+ const isKnown = (w) => lexicon.hasOwnProperty(w)
6
+ const scriptOnly = new Set(['Kanji', 'Hiragana', 'Katakana', 'Ascii'])
7
+
8
+ // a verb we've never seen can still be read off its ending.
9
+ // 「散歩した」 is a verb even if 散歩する isn't in the lexicon.
10
+ const tagUnknownVerbs = function (terms, setTag, world) {
11
+ terms.forEach(term => {
12
+ // only guess at words the lexicon hasn't already resolved.
13
+ // すみません is an expression, not the negative of a verb 済む.
14
+ let known = [...term.tags].some(t => !scriptOnly.has(t))
15
+ if (known) {
16
+ return
17
+ }
18
+ if (term.text.length < 2) {
19
+ return
20
+ }
21
+ let found = deconjugate(term.text, isKnown)
22
+ if (!found || found.guess === true) {
23
+ return
24
+ }
25
+ found.tags.forEach(tag => setTag([term], tag, world, null, reason))
26
+ term.root = found.root
27
+ })
28
+ }
29
+
30
+ // する attaches to the noun before it - 「勉強 します」
31
+ const tagSuruVerbs = function (terms, setTag, world) {
32
+ for (let i = 1; i < terms.length; i += 1) {
33
+ let t = terms[i]
34
+ if (!t.tags.has('Verb')) {
35
+ continue
36
+ }
37
+ if (/^(し|す|さ)/.test(t.text) === false) {
38
+ continue
39
+ }
40
+ let prev = terms[i - 1]
41
+ if (prev && prev.tags.has('SuruVerb')) {
42
+ setTag([prev], 'Verb', world, null, 'suruVerb')
43
+ }
44
+ }
45
+ }
46
+
47
+ // 〜て + いる/ある/おく/しまう - the helper verb carries the aspect,
48
+ // but the meaning belongs to the て-form in front of it
49
+ // 〜ている marks aspect; 〜てしまう, 〜てくれる and friends only add nuance
50
+ const aspectVerbs = ['いる', '居る', 'おる', 'ある']
51
+ const otherHelpers = ['おく', 'しまう', '来る', 'くる', '行く', 'いく', 'みる', '見る', 'あげる', 'くれる', 'もらう']
52
+
53
+ // every surface-form of every helper, so we can recognise いました / しまった
54
+ const formsOf = function (list) {
55
+ let set = new Set()
56
+ list.forEach(dict => {
57
+ let forms = conjugateVerb(dict)
58
+ if (forms) {
59
+ Object.keys(forms).forEach(k => typeof forms[k] === 'string' && set.add(forms[k]))
60
+ }
61
+ })
62
+ return set
63
+ }
64
+ let isAspect = formsOf(aspectVerbs)
65
+ let isHelper = formsOf(otherHelpers)
66
+
67
+ const tagCompoundVerbs = function (terms, setTag, world) {
68
+ for (let i = 1; i < terms.length; i += 1) {
69
+ let t = terms[i]
70
+ let prev = terms[i - 1]
71
+ if (!prev || !prev.tags.has('Gerund') || !t.tags.has('Verb')) {
72
+ continue
73
+ }
74
+ if (!isAspect.has(t.text) && !isHelper.has(t.text)) {
75
+ continue
76
+ }
77
+ if (isAspect.has(t.text)) {
78
+ setTag([prev], 'Progressive', world, null, 'compoundVerb')
79
+ }
80
+ ;['PastTense', 'Negative', 'Polite'].forEach(tag => {
81
+ if (t.tags.has(tag)) {
82
+ setTag([prev], tag, world, null, 'compoundVerb')
83
+ }
84
+ })
85
+ }
86
+ }
87
+
88
+ // an auxiliary hands its tense back to the verb it attaches to
89
+ const tagAuxiliary = function (terms, setTag, world) {
90
+ for (let i = 1; i < terms.length; i += 1) {
91
+ let t = terms[i]
92
+ if (!t.tags.has('Auxiliary')) {
93
+ continue
94
+ }
95
+ let prev = terms[i - 1]
96
+ if (!prev) {
97
+ continue
98
+ }
99
+ if (!prev.tags.has('Verb') && !prev.tags.has('Adjective') && !prev.tags.has('Noun')) {
100
+ setTag([prev], 'Verb', world, null, 'beforeAuxiliary')
101
+ }
102
+ // ませんでした after 行き makes the whole phrase past-polite-negative
103
+ ;['PastTense', 'PresentTense', 'Negative', 'Polite'].forEach(tag => {
104
+ if (t.tags.has(tag) && prev.tags.has('Verb')) {
105
+ setTag([prev], tag, world, null, 'fromAuxiliary')
106
+ }
107
+ })
108
+ }
109
+ }
110
+
111
+ export default { tagUnknownVerbs, tagSuruVerbs, tagAuxiliary, tagCompoundVerbs }
@@ -0,0 +1,43 @@
1
+ import { isKanji } from '../../../01-one/tokenizer/methods/lib.js'
2
+
3
+ const reason = 'adjSuffix'
4
+
5
+ // い-adjective endings. the old rule was 'ends in い → Adjective', which
6
+ // swallowed every negative verb (行かない), every masu-stem noun (お願い),
7
+ // and 弟/はい/せい besides.
8
+ const looksAdjective = /(し|た|な|か|ら|わ|が|ば|ざ|さ|は|ま|よ|る|す|つ|ぬ|ぶ|む|ぐ|ゆ|ろ|そ|お|こ|の|ほ|も|ど|ぼ|ぽ|ご|ぞ|づ|べ|で|げ|ぜ|え|け|せ|て|ね|へ|め|れ)い$/
9
+ // endings that look like an い-adjective but aren't
10
+ const notAdjective = /(ない|たい|らしい|っぽい)$/
11
+
12
+ const adjSuffixes = function (terms, setTag, world) {
13
+ terms.forEach(term => {
14
+ let str = term.text
15
+ if (term.tags.size > 1 || term.tags.has('Verb') || term.tags.has('Adjective')) {
16
+ return
17
+ }
18
+ if (str.length < 3 || !str.endsWith('い')) {
19
+ return
20
+ }
21
+ // ない/たい are verb endings, and are already handled as such
22
+ if (notAdjective.test(str) && !term.tags.has('Adjective')) {
23
+ return
24
+ }
25
+ // 美しい - kanji stem plus a hiragana tail is the classic shape
26
+ if (isKanji(str[0]) && looksAdjective.test(str)) {
27
+ setTag([term], 'Adjective', world, null, reason)
28
+ setTag([term], 'IAdjective', world, null, reason)
29
+ }
30
+ })
31
+ }
32
+
33
+ // 〜さ nominalises an adjective (高さ), 〜たち pluralises a noun (子供たち)
34
+ const nounSuffixes = function (terms, setTag, world) {
35
+ terms.forEach(term => {
36
+ if (term.text.length > 2 && /(たち|達)$/.test(term.text)) {
37
+ setTag([term], 'Plural', world, null, 'nounSuffix')
38
+ setTag([term], 'Noun', world, null, 'nounSuffix')
39
+ }
40
+ })
41
+ }
42
+
43
+ export default { adjSuffixes, nounSuffixes }
@@ -0,0 +1,22 @@
1
+ const reason = 'honorific'
2
+
3
+ // 田中さん - an honorific proves the word before it is a person
4
+ const tagPeople = function (terms, setTag, world) {
5
+ for (let i = 0; i < terms.length; i += 1) {
6
+ let t = terms[i]
7
+ if (!t.tags.has('Honorific')) {
8
+ continue
9
+ }
10
+ let prev = terms[i - 1]
11
+ if (prev && !prev.tags.has('Verb') && !prev.tags.has('Particle')) {
12
+ setTag([prev], 'Person', world, null, reason)
13
+ }
14
+ }
15
+ // 田中さん may have tokenized as one word - the suffix is still a signal
16
+ terms.forEach(term => {
17
+ if (term.text.length > 2 && /(さん|様|さま|ちゃん|くん|氏)$/.test(term.text) && term.tags.size <= 1) {
18
+ setTag([term], 'Person', world, null, reason)
19
+ }
20
+ })
21
+ }
22
+ export default tagPeople