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.
- package/LICENSE +21 -0
- package/README.md +367 -6
- package/builds/ja-compromise.cjs +4389 -2286
- package/builds/ja-compromise.min.js +1 -1
- package/builds/ja-compromise.mjs +1 -1
- package/package.json +36 -23
- package/src/01-one/conjugate/conjugate-adj.js +53 -0
- package/src/01-one/conjugate/conjugate-verb.js +145 -0
- package/src/01-one/conjugate/deconjugate.js +178 -0
- package/src/01-one/conjugate/index.js +13 -0
- package/src/01-one/conjugate/kana.js +38 -0
- package/src/01-one/conjugate/tags.js +85 -0
- package/src/01-one/conjugate/verb-class.js +105 -0
- package/src/01-one/lexicon/_data.js +30 -0
- package/src/01-one/lexicon/api.js +61 -0
- package/src/01-one/lexicon/lexicon.js +163 -0
- package/src/01-one/lexicon/misc.js +115 -0
- package/src/01-one/lexicon/plugin.js +20 -0
- package/src/01-one/numbers/api.js +202 -0
- package/src/01-one/numbers/kanji-number.js +69 -0
- package/src/01-one/numbers/to-kanji.js +66 -0
- package/src/01-one/output/compute/dict.js +13 -0
- package/src/01-one/output/compute/english.js +13 -0
- package/src/01-one/output/compute/root.js +30 -0
- package/src/01-one/output/debug/_color.js +16 -0
- package/src/01-one/output/debug/index.js +24 -0
- package/src/01-one/output/debug/tags.js +56 -0
- package/src/01-one/output/plugin.js +13 -0
- package/src/01-one/romanji/api.js +16 -0
- package/src/01-one/romanji/compute/index.js +36 -0
- package/src/01-one/romanji/compute/kanji-reading/index.js +91 -0
- package/src/01-one/romanji/compute/kanji-reading/readings.js +861 -0
- package/src/01-one/romanji/compute/kanji-reading/words.js +31 -0
- package/src/01-one/romanji/compute/toRomanji/hiragana-map.js +175 -0
- package/src/01-one/romanji/compute/toRomanji/index.js +84 -0
- package/src/01-one/romanji/compute/toRomanji/katakana-map.js +128 -0
- package/src/01-one/romanji/plugin.js +7 -0
- package/src/01-one/tokenizer/methods/join-numbers.js +32 -0
- package/src/01-one/tokenizer/methods/join-up.js +66 -0
- package/src/01-one/tokenizer/methods/lib.js +66 -0
- package/src/01-one/tokenizer/methods/naiive-split.js +88 -0
- package/src/01-one/tokenizer/methods/okurigana.js +58 -0
- package/src/01-one/tokenizer/methods/terms.js +143 -0
- package/src/01-one/tokenizer/methods/trie/build.js +13 -0
- package/src/01-one/tokenizer/methods/trie/split-up.js +29 -0
- package/src/01-one/tokenizer/methods/whitespace.js +44 -0
- package/src/01-one/tokenizer/plugin.js +13 -0
- package/src/02-two/preTagger/compute/01-script.js +44 -0
- package/src/02-two/preTagger/compute/02-particles.js +82 -0
- package/src/02-two/preTagger/compute/03-verbs.js +111 -0
- package/src/02-two/preTagger/compute/04-adjectives.js +43 -0
- package/src/02-two/preTagger/compute/05-people.js +22 -0
- package/src/02-two/preTagger/compute/06-numbers.js +80 -0
- package/src/02-two/preTagger/compute/07-dates.js +105 -0
- package/src/02-two/preTagger/compute/index.js +52 -0
- package/src/02-two/preTagger/plugin.js +9 -0
- package/src/02-two/tagset/plugin.js +12 -0
- package/src/02-two/tagset/tags/dates.js +58 -0
- package/src/02-two/tagset/tags/misc.js +91 -0
- package/src/02-two/tagset/tags/nouns.js +131 -0
- package/src/02-two/tagset/tags/particles.js +50 -0
- package/src/02-two/tagset/tags/values.js +68 -0
- package/src/02-two/tagset/tags/verbs.js +93 -0
- package/src/_lib.js +2 -0
- package/src/_version.js +1 -0
- package/src/index.js +82 -0
- package/types/index.d.ts +268 -0
- package/types/japanese.d.ts +159 -0
- 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
|