chaffjs 0.13.0 → 0.14.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/dist/detectors/acronym-context.d.ts +2 -0
- package/dist/detectors/acronym-context.d.ts.map +1 -1
- package/dist/detectors/acronym-context.js +7 -0
- package/dist/detectors/acronym-context.js.map +1 -1
- package/dist/detectors/char-shape.d.ts.map +1 -1
- package/dist/detectors/char-shape.js +2 -2
- package/dist/detectors/char-shape.js.map +1 -1
- package/dist/detectors/conjugated-form.d.ts +3 -0
- package/dist/detectors/conjugated-form.d.ts.map +1 -0
- package/dist/detectors/conjugated-form.js +29 -0
- package/dist/detectors/conjugated-form.js.map +1 -0
- package/dist/detectors/enumerated-runs.d.ts +22 -0
- package/dist/detectors/enumerated-runs.d.ts.map +1 -0
- package/dist/detectors/enumerated-runs.js +53 -0
- package/dist/detectors/enumerated-runs.js.map +1 -0
- package/dist/detectors/middle-dot.d.ts +6 -0
- package/dist/detectors/middle-dot.d.ts.map +1 -0
- package/dist/detectors/middle-dot.js +9 -0
- package/dist/detectors/middle-dot.js.map +1 -0
- package/dist/detectors/orthography.d.ts.map +1 -1
- package/dist/detectors/orthography.js +6 -4
- package/dist/detectors/orthography.js.map +1 -1
- package/dist/detectors/polite-word.d.ts +4 -0
- package/dist/detectors/polite-word.d.ts.map +1 -0
- package/dist/detectors/polite-word.js +10 -0
- package/dist/detectors/polite-word.js.map +1 -0
- package/dist/detectors/register.d.ts +7 -5
- package/dist/detectors/register.d.ts.map +1 -1
- package/dist/detectors/register.js +6 -4
- package/dist/detectors/register.js.map +1 -1
- package/dist/detectors/roman-numeral.d.ts +6 -0
- package/dist/detectors/roman-numeral.d.ts.map +1 -0
- package/dist/detectors/roman-numeral.js +6 -0
- package/dist/detectors/roman-numeral.js.map +1 -0
- package/dist/detectors/sentence-ending.d.ts.map +1 -1
- package/dist/detectors/sentence-ending.js +14 -4
- package/dist/detectors/sentence-ending.js.map +1 -1
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +8 -1
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/structure.d.ts.map +1 -1
- package/dist/detectors/structure.js +2 -1
- package/dist/detectors/structure.js.map +1 -1
- package/dist/number-name.d.ts +3 -2
- package/dist/number-name.d.ts.map +1 -1
- package/dist/number-name.js +25 -6
- package/dist/number-name.js.map +1 -1
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/preamble-paragraphs.d.ts +7 -0
- package/dist/preamble-paragraphs.d.ts.map +1 -0
- package/dist/preamble-paragraphs.js +7 -0
- package/dist/preamble-paragraphs.js.map +1 -0
- package/dist/structure/build.d.ts.map +1 -1
- package/dist/structure/build.js +7 -4
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/heading-leaves.d.ts +7 -0
- package/dist/structure/heading-leaves.d.ts.map +1 -0
- package/dist/structure/heading-leaves.js +6 -0
- package/dist/structure/heading-leaves.js.map +1 -0
- package/package.json +3 -3
- package/rules/duplicate-definition.yaml +2 -2
- package/rules/latin-spacing.yaml +2 -2
- package/rules/no-nakaguro-parallel.yaml +2 -2
- package/rules/undefined-acronym.yaml +2 -1
- package/src/detectors/acronym-context.ts +10 -0
- package/src/detectors/char-shape.ts +2 -3
- package/src/detectors/conjugated-form.ts +40 -0
- package/src/detectors/enumerated-runs.ts +68 -0
- package/src/detectors/middle-dot.ts +10 -0
- package/src/detectors/orthography.ts +6 -4
- package/src/detectors/polite-word.ts +14 -0
- package/src/detectors/register.ts +11 -7
- package/src/detectors/roman-numeral.ts +5 -0
- package/src/detectors/sentence-ending.ts +23 -6
- package/src/detectors/signals.ts +9 -1
- package/src/detectors/structure.ts +2 -1
- package/src/number-name.ts +34 -6
- package/src/plugin.ts +2 -0
- package/src/preamble-paragraphs.ts +9 -0
- package/src/structure/build.ts +13 -10
- package/src/structure/heading-leaves.ts +7 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Sentence, Token } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 語彙表が原形で書いた語句(という、と呼ぶ)が、文の中で活用して書かれた形(といいます、と呼びました)。
|
|
5
|
+
* 語句の最後の動詞を原形(lemma)で照らし、その前の部分は字面で照らす。語句を語に分けて並びで照らさないのは、
|
|
6
|
+
* 解析器が単独の「という」を 1 つの助詞に、「といいます」を と・いい・ます に分け、「称し」の原形を 称す とするから。
|
|
7
|
+
* 活用した形は、動詞に続く助動詞(ます、た)まで。品詞が無い文からは何も出ない。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
type Run = { readonly start: number; readonly end: number };
|
|
11
|
+
|
|
12
|
+
/** 動詞と、そのすぐ後ろに続く助動詞の、文の中の範囲。 */
|
|
13
|
+
const inflectedRun = (sentence: Sentence, tokens: readonly Token[], index: number, verb: Token): Run => {
|
|
14
|
+
const rest = tokens.slice(index + 1);
|
|
15
|
+
const stop = rest.findIndex((token) => token.pos !== "AUX");
|
|
16
|
+
const last = (stop === -1 ? rest : rest.slice(0, stop)).at(-1) ?? verb;
|
|
17
|
+
return { start: verb.span.start - sentence.span.start, end: last.span.end - sentence.span.start };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const formsAt = (sentence: Sentence, tokens: readonly Token[], index: number, bases: readonly string[]): string[] => {
|
|
21
|
+
const verb = tokens[index];
|
|
22
|
+
const lemma = verb?.lemma;
|
|
23
|
+
if (verb?.pos !== "VERB" || lemma === undefined || lemma === "") return [];
|
|
24
|
+
const run = inflectedRun(sentence, tokens, index, verb);
|
|
25
|
+
const before = sentence.text.slice(0, run.start);
|
|
26
|
+
const written = sentence.text.slice(run.start, run.end);
|
|
27
|
+
return bases
|
|
28
|
+
.filter((base) => base.endsWith(lemma))
|
|
29
|
+
.map((base) => base.slice(0, base.length - lemma.length))
|
|
30
|
+
.filter((head) => before.endsWith(head))
|
|
31
|
+
.map((head) => `${head}${written}`);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const conjugatedForms = (sentences: readonly Sentence[], bases: readonly string[]): string[] => {
|
|
35
|
+
const forms = sentences.flatMap((sentence) => {
|
|
36
|
+
const tokens = sentence.tokens ?? [];
|
|
37
|
+
return tokens.flatMap((_token, index) => formsAt(sentence, tokens, index, bases));
|
|
38
|
+
});
|
|
39
|
+
return [...new Set(forms)];
|
|
40
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Sentence, Span, StructureNode, Token } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
/** 番号の付いた行。start は行の始まり(字下げの前)、label は言語パッケージが読んだ番号(「(1)」)。 */
|
|
4
|
+
export type NumberedStart = { readonly start: number; readonly label: string };
|
|
5
|
+
|
|
6
|
+
const labelOf = (node: StructureNode): string => {
|
|
7
|
+
const label = node.attrs["label"];
|
|
8
|
+
return typeof label === "string" ? label : "";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 条の外で、番号の付いた行(言語パッケージが項目と読んだ「(1)」など)。
|
|
13
|
+
* 条の中の番号付きの行は、条の項(条の本文の続き)でもありうるので数えない。
|
|
14
|
+
*/
|
|
15
|
+
export const numberedStarts = (node: StructureNode, inArticle = false): NumberedStart[] => [
|
|
16
|
+
...(node.kind === "item" && !inArticle ? [{ start: node.span.start, label: labelOf(node) }] : []),
|
|
17
|
+
...node.children.flatMap((child) => numberedStarts(child, inArticle || node.kind === "article")),
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const INDENT = /^[ \t\u3000]*/u;
|
|
21
|
+
|
|
22
|
+
const lineAt = (source: string, start: number): string => {
|
|
23
|
+
const end = source.indexOf("\n", start);
|
|
24
|
+
return source.slice(start, end === -1 ? source.length : end);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** 番号の直後の語。字下げと番号と空白を飛ばした先の、最初の語。 */
|
|
28
|
+
const wordAfter = (numbered: NumberedStart, sentences: readonly Sentence[], source: string): Token | undefined => {
|
|
29
|
+
const after = numbered.start + (INDENT.exec(lineAt(source, numbered.start))?.[0].length ?? 0) + numbered.label.length;
|
|
30
|
+
const sentence = sentences.find((candidate) => candidate.span.end > after);
|
|
31
|
+
return sentence?.tokens?.find((token) => token.span.start >= after && token.surface.trim() !== "");
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 並びの項目として番号を振った行の始まり。番号の直後が助詞(「(1)の金額は」「(2)は対象外」)なら、
|
|
36
|
+
* 番号は項目の印ではなく、項目を指す本文の主語や修飾なので除く。
|
|
37
|
+
*/
|
|
38
|
+
export const enumeratorStarts = (numbered: readonly NumberedStart[], sentences: readonly Sentence[], source: string): number[] =>
|
|
39
|
+
numbered.filter((line) => wordAfter(line, sentences, source)?.pos !== "ADP").map((line) => line.start);
|
|
40
|
+
|
|
41
|
+
/** 番号の付いた行の始まりと段落の始まりのあいだが字下げだけなら、段落はその行から始まる。 */
|
|
42
|
+
const opensWithItem = (paragraph: Span, starts: readonly number[], source: string): boolean =>
|
|
43
|
+
starts.some((start) => start <= paragraph.start && source.slice(start, paragraph.start).trim() === "");
|
|
44
|
+
|
|
45
|
+
const isBlank = (source: string, from: number, to: number): boolean => source.slice(from, to).trim() === "";
|
|
46
|
+
|
|
47
|
+
/** 並びと読むのに要る、番号の付いた行の数。一つだけなら、並びではなく番号で始まる本文の段落かもしれない。 */
|
|
48
|
+
const MIN_ITEMS = 2;
|
|
49
|
+
|
|
50
|
+
/** 番号の行の始まりは字下げの前なので、並びの最初の行の頭から数える。 */
|
|
51
|
+
const itemsIn = (run: Span, starts: readonly number[], source: string): number => {
|
|
52
|
+
const lineStart = source.lastIndexOf("\n", run.start - 1) + 1;
|
|
53
|
+
return starts.filter((start) => start >= lineStart && start < run.end).length;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 番号で始まる段落が、空行だけを挟んで続く範囲。番号の付いた行を二つ以上含むものだけ。
|
|
58
|
+
* 要件や条件を「(1)…であること。」と並べるのは、段落でも箇条書きと同じ書き方で、本文の調子とは別に揃える。
|
|
59
|
+
*/
|
|
60
|
+
export const enumeratedRuns = (paragraphs: readonly Span[], starts: readonly number[], source: string): Span[] => {
|
|
61
|
+
const enumerated = paragraphs.filter((paragraph) => opensWithItem(paragraph, starts, source));
|
|
62
|
+
const runs = enumerated.reduce<Span[]>((joined, paragraph) => {
|
|
63
|
+
const last = joined.at(-1);
|
|
64
|
+
if (last === undefined || !isBlank(source, last.end, paragraph.start)) return [...joined, paragraph];
|
|
65
|
+
return [...joined.slice(0, -1), { start: last.start, end: paragraph.end }];
|
|
66
|
+
}, []);
|
|
67
|
+
return runs.filter((run) => itemsIn(run, starts, source) >= MIN_ITEMS);
|
|
68
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const MIDDLE_DOT = /・/gu;
|
|
2
|
+
|
|
3
|
+
/** 文頭か行頭(前に空白だけ)の「・」。前に並べる項目が無いので、箇条書きの印。 */
|
|
4
|
+
const BULLET_DOT = /(^|\n)[^\S\n]*・/gu;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 項目を並べる中黒の数。箇条書きの印の「・」は数えない。
|
|
8
|
+
* 「・ 水分を補給すること」が何行続いても、どこまでが 1 つの項目かは改行で読める。
|
|
9
|
+
*/
|
|
10
|
+
export const parallelDotCount = (text: string): number => [...text.replace(BULLET_DOT, "$1").matchAll(MIDDLE_DOT)].length;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Detector, Finding, Sentence } from "../plugin.ts";
|
|
2
2
|
import { latinBoundaries, minorityStyle, occurrencesOutside, type Boundary, type SpacingKind } from "../orthography.ts";
|
|
3
|
-
import { digitRunAround, isNumberName } from "../number-name.ts";
|
|
3
|
+
import { digitRunAround, isNumberName, sequenceLabelStarts } from "../number-name.ts";
|
|
4
4
|
|
|
5
5
|
/** チームが chaff.yaml の prefer に書いた「使わない書き方」。書いていなければ何も言わない。 */
|
|
6
6
|
export const preferredTerm: Detector = (doc, options): Finding[] => {
|
|
@@ -31,10 +31,10 @@ const digitBeside = (boundary: Boundary): number => {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
/** 番号・識別子として書かれた数の境目は、空け方の好みではないので数えない(number-name.ts)。 */
|
|
34
|
-
const isCounted = (sentence: Sentence, boundary: Boundary): boolean => {
|
|
34
|
+
const isCounted = (sentence: Sentence, boundary: Boundary, sequence: ReadonlySet<number>): boolean => {
|
|
35
35
|
if (boundary.kind === "letter") return true;
|
|
36
36
|
const run = digitRunAround(sentence.text, digitBeside(boundary));
|
|
37
|
-
return run === undefined || !isNumberName(sentence.text, run, sentence.tokens, sentence.span.start);
|
|
37
|
+
return run === undefined || !isNumberName(sentence.text, run, sentence.tokens, sentence.span.start, sequence);
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -42,9 +42,11 @@ const isCounted = (sentence: Sentence, boundary: Boundary): boolean => {
|
|
|
42
42
|
* どちらが正しいかは決めない。決めるのはチームで、chaff はそろっているかだけを見る。
|
|
43
43
|
*/
|
|
44
44
|
export const latinSpacing: Detector = (doc, options): Finding[] => {
|
|
45
|
+
// 覆った文(prose)で探す。コードの中の「1 件」は並びに入れない。
|
|
46
|
+
const sequence = sequenceLabelStarts(doc.prose ?? doc.source);
|
|
45
47
|
const located: Located[] = doc.sentences.flatMap((sentence) =>
|
|
46
48
|
latinBoundaries(sentence.text, doc.source.slice(sentence.span.start, sentence.span.end))
|
|
47
|
-
.filter((boundary) => isCounted(sentence, boundary))
|
|
49
|
+
.filter((boundary) => isCounted(sentence, boundary, sequence))
|
|
48
50
|
.map((boundary) => ({ sentence, ...boundary })),
|
|
49
51
|
);
|
|
50
52
|
return KINDS.flatMap((kind) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LexiconEntry, Token } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
/** 語彙表の語が解析器で一語になったときの、その語。二語以上(「ございます」)は読みで照らさない。 */
|
|
4
|
+
const singleWord = (entry: LexiconEntry): Token | undefined => (entry.tokens?.length === 1 ? entry.tokens[0] : undefined);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 読みと品詞が同じなら、表記が違っても同じ語の同じ形。解析器は「下さい」の原形を「下さる」、「ください」の原形を「くださる」と
|
|
8
|
+
* 別の字で返すが、読み(クダサイ)と品詞は同じ。品詞も比べるのは、読みだけだと「升(マス)」が「ます」に当たるから。
|
|
9
|
+
*/
|
|
10
|
+
const sameReading = (token: Token, word: Token | undefined): boolean => word?.reading !== undefined && token.reading === word.reading && token.pos === word.pos;
|
|
11
|
+
|
|
12
|
+
/** 語が丁寧語の語彙表の語か。書いた形か原形が語彙表の語と同じか、語彙表の一語と読みと品詞が同じ。 */
|
|
13
|
+
export const isPoliteWord = (token: Token, entries: readonly LexiconEntry[]): boolean =>
|
|
14
|
+
entries.some((entry) => token.surface === entry.pattern || token.lemma === entry.pattern || sameReading(token, singleWord(entry)));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isPoliteWord } from "./polite-word.ts";
|
|
2
|
+
import type { LexiconEntry, Sentence, Span, Token } from "../plugin.ts";
|
|
2
3
|
|
|
3
4
|
export type Register = "polite" | "plain";
|
|
4
5
|
|
|
@@ -25,13 +26,12 @@ const withPreceding = (ending: readonly Token[], preceding: readonly Token[]): r
|
|
|
25
26
|
* 文末の語の調子。preceding は文末の語より前の語。
|
|
26
27
|
* 文末に述語(動詞・形容詞・助動詞)が無ければ、ですます調でもである調でもないので undefined。
|
|
27
28
|
* 「以下の通り。」「円錐形の麦わら帽子。」のような名詞で終わる文を数えると、そのままである調の少数派になっていた。
|
|
28
|
-
*
|
|
29
|
+
* 書いた形でも原形でも読みでも当てる(polite-word.ts)。「ください」の原形は「くださる」で、原形だけを見ると丁寧な文末を見落とす。
|
|
29
30
|
*/
|
|
30
|
-
export const registerOf = (ending: readonly Token[], preceding: readonly Token[], polite: readonly
|
|
31
|
+
export const registerOf = (ending: readonly Token[], preceding: readonly Token[], polite: readonly LexiconEntry[]): Register | undefined => {
|
|
31
32
|
const judged = withPreceding(ending, preceding);
|
|
32
33
|
if (!judged.some(isPredicate)) return undefined;
|
|
33
|
-
|
|
34
|
-
return isPolite ? "polite" : "plain";
|
|
34
|
+
return judged.some((token) => isPoliteWord(token, polite)) ? "polite" : "plain";
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
const EXCLAIMED = /[!?!?]$/u;
|
|
@@ -49,7 +49,11 @@ export const outermostList = (offset: number, lists: readonly Span[]): number |
|
|
|
49
49
|
.filter((list) => offset >= list.start && offset < list.end)
|
|
50
50
|
.reduce<number | undefined>((outer, list) => (outer === undefined || list.start < outer ? list.start : outer), undefined);
|
|
51
51
|
|
|
52
|
-
/**
|
|
52
|
+
/** 文が入っているまとまりの始まり。一番外側の箇条書き、それが無ければ番号で始まる段落の並び(runs)。本文なら undefined。 */
|
|
53
|
+
export const groupOf = (offset: number, lists: readonly Span[], runs: readonly Span[]): number | undefined =>
|
|
54
|
+
outermostList(offset, lists) ?? outermostList(offset, runs);
|
|
55
|
+
|
|
56
|
+
/** group: 文が入っているまとまり(groupOf の値)。本文なら undefined。 */
|
|
53
57
|
export type Judged = { readonly register: Register; readonly group: number | undefined };
|
|
54
58
|
|
|
55
59
|
/** 混ざった文と、その文が属する本文または箇条書きの中の少数派の数。 */
|
|
@@ -74,7 +78,7 @@ const slipsInGroup = <T extends Judged>(judged: readonly T[], group: number | un
|
|
|
74
78
|
};
|
|
75
79
|
|
|
76
80
|
/**
|
|
77
|
-
* 調子が混ざった文を、judged
|
|
81
|
+
* 調子が混ざった文を、judged の順で。本文は本文どうし、箇条書きと番号で始まる段落の並びは 1 つずつ、その中で揃っているかを見る。
|
|
78
82
|
* ですます調の本文に常体の箇条書きを置くのはよくある書き方で、箇条書きが丸ごと揃っていれば混在ではない。
|
|
79
83
|
*/
|
|
80
84
|
export const slipsOf = <T extends Judged>(judged: readonly T[], limit: number): Slip<T>[] => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { endingTokens, isClosed } from "../sentence-shape.ts";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { enumeratedRuns, enumeratorStarts, numberedStarts } from "./enumerated-runs.ts";
|
|
3
|
+
import { continuesInto, groupOf, registerOf, slipsOf, type Register } from "./register.ts";
|
|
4
|
+
import type { Detector, Finding, LexiconEntry, ProseDocument, Sentence, Span } from "../plugin.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 文末の調子が混ざっているかを見る。日本語の「ですます / である」がこれ。
|
|
@@ -11,21 +12,37 @@ import type { Detector, Finding, Sentence } from "../plugin.ts";
|
|
|
11
12
|
*/
|
|
12
13
|
type Entry = { readonly sentence: Sentence; readonly register: Register; readonly group: number | undefined };
|
|
13
14
|
|
|
14
|
-
const registerOfSentence = (sentence: Sentence, polite: readonly
|
|
15
|
+
const registerOfSentence = (sentence: Sentence, polite: readonly LexiconEntry[]): Register | undefined => {
|
|
15
16
|
const ending = endingTokens(sentence);
|
|
16
17
|
const tokens = sentence.tokens ?? [];
|
|
17
18
|
const head = ending[0] === undefined ? -1 : tokens.indexOf(ending[0]);
|
|
18
19
|
return registerOf(ending, head > 0 ? tokens.slice(0, head) : [], polite);
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
/** 番号で始まる段落の並び。木を組むのは高いので、調子が一つだけの文書(混ざりようがない)では組まない。 */
|
|
23
|
+
const runsOf = (doc: ProseDocument, registers: readonly Register[]): Span[] => {
|
|
24
|
+
const tree = new Set(registers).size > 1 ? doc.structure : undefined;
|
|
25
|
+
if (tree === undefined) return [];
|
|
26
|
+
return enumeratedRuns(
|
|
27
|
+
doc.paragraphs.map((paragraph) => paragraph.span),
|
|
28
|
+
enumeratorStarts(numberedStarts(tree), doc.sentences, doc.source),
|
|
29
|
+
doc.source,
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
21
33
|
export const sentenceEnding: Detector = (doc, options): Finding[] => {
|
|
22
|
-
const polite =
|
|
34
|
+
const polite = options.lexicon ?? [];
|
|
23
35
|
const lists = doc.lists.map((list) => list.span);
|
|
24
|
-
const
|
|
36
|
+
const found = doc.sentences.flatMap((sentence, index): { sentence: Sentence; register: Register }[] => {
|
|
25
37
|
if (!isClosed(sentence) || continuesInto(sentence, doc.sentences[index + 1])) return [];
|
|
26
38
|
const register = registerOfSentence(sentence, polite);
|
|
27
|
-
return register === undefined ? [] : [{ sentence, register
|
|
39
|
+
return register === undefined ? [] : [{ sentence, register }];
|
|
28
40
|
});
|
|
41
|
+
const runs = runsOf(
|
|
42
|
+
doc,
|
|
43
|
+
found.map((entry) => entry.register),
|
|
44
|
+
);
|
|
45
|
+
const judged = found.map((entry): Entry => ({ ...entry, group: groupOf(entry.sentence.span.start, lists, runs) }));
|
|
29
46
|
return slipsOf(judged, options.limit).map(({ entry: { sentence }, count }) => ({
|
|
30
47
|
rule: "no-mixed-desumasu",
|
|
31
48
|
severity: "warning",
|
package/src/detectors/signals.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { wordsOf } from "./structure.ts";
|
|
|
3
3
|
import { compacted, placeOf } from "./gram-place.ts";
|
|
4
4
|
import { notAcronymSpansOf, type NotAcronymSpans } from "./acronym-context.ts";
|
|
5
5
|
import { expansionAt, type ExpandedAt } from "./acronym-expansion.ts";
|
|
6
|
+
import { conjugatedForms } from "./conjugated-form.ts";
|
|
6
7
|
import type { Detector, Finding, ProseDocument, Section, Sentence, Token } from "../plugin.ts";
|
|
7
8
|
|
|
8
9
|
const PER = 1000;
|
|
@@ -216,8 +217,15 @@ const notationOf = (doc: ProseDocument): NotAcronymSpans =>
|
|
|
216
217
|
currencies: patternsOf(doc, "currency-code"),
|
|
217
218
|
usStates: patternsOf(doc, "us-state-code"),
|
|
218
219
|
emphasis: patternsOf(doc, "emphasis-word"),
|
|
220
|
+
divisions: patternsOf(doc, "numbered-division"),
|
|
219
221
|
});
|
|
220
222
|
|
|
223
|
+
/** 定義の語は、語彙表の形(という)と、この文書で活用して書かれた形(といいます)の両方で照らす。 */
|
|
224
|
+
const definitionVerbsOf = (doc: ProseDocument): string[] => {
|
|
225
|
+
const verbs = patternsOf(doc, "definition-verb");
|
|
226
|
+
return [...verbs, ...conjugatedForms(doc.sentences, verbs)];
|
|
227
|
+
};
|
|
228
|
+
|
|
221
229
|
export const undefinedAcronym: Detector = (doc, options): Finding[] => {
|
|
222
230
|
const body = bodyOf(doc);
|
|
223
231
|
const common = new Set((options.lexicon ?? []).map((entry) => entry.pattern));
|
|
@@ -225,7 +233,7 @@ export const undefinedAcronym: Detector = (doc, options): Finding[] => {
|
|
|
225
233
|
acronymsOf(doc, notationOf(doc)).forEach(({ word, hit }) => {
|
|
226
234
|
if (!common.has(word) && !seen.has(word)) seen.set(word, hit);
|
|
227
235
|
});
|
|
228
|
-
const expandedAt = expansionAt({ markers: patternsOf(doc, "definition-marker"), verbs:
|
|
236
|
+
const expandedAt = expansionAt({ markers: patternsOf(doc, "definition-marker"), verbs: definitionVerbsOf(doc) });
|
|
229
237
|
const bare = [...seen.entries()].filter(([acronym]) => !isExpanded(body, acronym, expandedAt));
|
|
230
238
|
if (bare.length < options.limit) return [];
|
|
231
239
|
return bare.map(([acronym, hit]) => ({
|
|
@@ -2,6 +2,7 @@ import { charLength, lengthOf } from "../measure.ts";
|
|
|
2
2
|
import type { BulletList, Detector, Finding, Paragraph, ProseDocument, Section, Span } from "../plugin.ts";
|
|
3
3
|
import { dateStampIndexes, stampCandidates } from "../date-stamp.ts";
|
|
4
4
|
import { inDocumentOrder } from "../structure/issues.ts";
|
|
5
|
+
import { preambleParagraphs } from "../preamble-paragraphs.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* ばらつきは変動係数(標準偏差 ÷ 平均)で測る。
|
|
@@ -116,7 +117,7 @@ const withoutDateStamps = (doc: ProseDocument, paragraphs: readonly Paragraph[])
|
|
|
116
117
|
export const preambleLength: Detector = (doc, options): Finding[] => {
|
|
117
118
|
const body = doc.sections.find((section) => section.depth >= 2);
|
|
118
119
|
if (body === undefined) return [];
|
|
119
|
-
const all = doc.paragraphs
|
|
120
|
+
const all = preambleParagraphs(doc.paragraphs, body.span.start);
|
|
120
121
|
if (all.length <= options.limit) return [];
|
|
121
122
|
const before = withoutDateStamps(doc, all);
|
|
122
123
|
if (before.length <= options.limit) return [];
|
package/src/number-name.ts
CHANGED
|
@@ -5,8 +5,8 @@ import type { Span, Token } from "./plugin.ts";
|
|
|
5
5
|
* 「〒102-0094 東京都」の郵便番号は、後ろの空白が番号と題・項目の区切りで、「3回」「3 回」のような空け方の好みではない。
|
|
6
6
|
* 空け方を数える材料にしない。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* 形で名前と読むのは四つだけ。0 で始まる組を含むハイフンつなぎの番号、※ や鉤括弧のすぐ後ろの番号、文頭の階層つきの節番号(2.1、3.1.2)、
|
|
9
|
+
* 続き番号になった行の頭の番号(白書の注)。文頭でも「223 言語」「1ターン」は数量なので、階層の無い数は文頭というだけでは名前と読まない。
|
|
10
10
|
* そのうえで、すぐ後ろ(空白 1 つまで)の語が数につく語(助数詞・数・助詞・助動詞)なら数量として数え続ける(「3-5 日」「26.7 万行」)。
|
|
11
11
|
* 品詞が無ければ判断せず、名前とは読まない。
|
|
12
12
|
*/
|
|
@@ -51,6 +51,26 @@ const wordAfter = (tokens: readonly Token[], text: string, run: Span, base: numb
|
|
|
51
51
|
const isBoundToNumber = (token: Token): boolean =>
|
|
52
52
|
token.features?.["NounType"] === "Class" || token.features?.["NumType"] === "Card" || BOUND_TO_NUMBER.has(token.pos);
|
|
53
53
|
|
|
54
|
+
/** 「1 又は 2」の接続詞は数どうしをつなぐが、続き番号の注の本文は「31 ただし、…」のように接続詞でも始まる。 */
|
|
55
|
+
const CONJUNCTIONS = new Set(["CCONJ", "SCONJ"]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 白書の注(「9 首相に…」「10 日本経済新聞…」)のように、行の頭に階層の無い番号を置き、次の行の頭の番号が 1 つ大きいもの。
|
|
59
|
+
* 一行だけ見ると「223 言語に対応」と区別がつかないので、前後の番号の行と続き番号になっているものだけを番号と読む。
|
|
60
|
+
* 後ろが英字(「15 Federal Bureau…」)の行も並びには数える。値は行の頭の位置(文書全体の座標)。
|
|
61
|
+
*/
|
|
62
|
+
const NUMBERED_LINE = /^[ \t]*(?:[-*+][ \t]+)?(?<number>\d{1,3})[ \t]+\S/gmu;
|
|
63
|
+
|
|
64
|
+
export const sequenceLabelStarts = (text: string): ReadonlySet<number> => {
|
|
65
|
+
const lines = [...text.matchAll(NUMBERED_LINE)].map((match) => {
|
|
66
|
+
const number = match.groups?.["number"] ?? "";
|
|
67
|
+
return { start: match.index + match[0].indexOf(number), value: Number(number) };
|
|
68
|
+
});
|
|
69
|
+
const continues = (index: number): boolean =>
|
|
70
|
+
lines[index - 1]?.value === (lines[index]?.value ?? 0) - 1 || lines[index + 1]?.value === (lines[index]?.value ?? 0) + 1;
|
|
71
|
+
return new Set(lines.flatMap((line, index) => (continues(index) ? [line.start] : [])));
|
|
72
|
+
};
|
|
73
|
+
|
|
54
74
|
/** ハイフンでつないだ識別子か、番号の立つ位置の番号か、文頭の節番号か。 */
|
|
55
75
|
const isNameShaped = (text: string, run: Span): boolean => {
|
|
56
76
|
const [before, digits] = [text.slice(0, run.start), text.slice(run.start, run.end)];
|
|
@@ -58,11 +78,19 @@ const isNameShaped = (text: string, run: Span): boolean => {
|
|
|
58
78
|
};
|
|
59
79
|
|
|
60
80
|
/**
|
|
61
|
-
* text の run が名前として書かれた数か。tokens は文書全体の座標で、base は text の先頭の位置。
|
|
81
|
+
* text の run が名前として書かれた数か。tokens は文書全体の座標で、base は text の先頭の位置。sequence は sequenceLabelStarts の結果。
|
|
62
82
|
* 後ろの語が読めない(品詞が無い、語の切れ目が合わない)ときは、数量として数え続ける。
|
|
63
83
|
*/
|
|
64
|
-
export const isNumberName = (
|
|
65
|
-
|
|
84
|
+
export const isNumberName = (
|
|
85
|
+
text: string,
|
|
86
|
+
run: Span,
|
|
87
|
+
tokens: readonly Token[] | undefined,
|
|
88
|
+
base: number,
|
|
89
|
+
sequence: ReadonlySet<number> = new Set(),
|
|
90
|
+
): boolean => {
|
|
91
|
+
const inSequence = sequence.has(base + run.start);
|
|
92
|
+
if (tokens === undefined || !(inSequence || isNameShaped(text, run))) return false;
|
|
66
93
|
const next = wordAfter(tokens, text, run, base);
|
|
67
|
-
|
|
94
|
+
if (next === undefined) return false;
|
|
95
|
+
return !isBoundToNumber(next) || (inSequence && CONJUNCTIONS.has(next.pos));
|
|
68
96
|
};
|
package/src/plugin.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type Token = {
|
|
|
20
20
|
readonly surface: string;
|
|
21
21
|
readonly pos: string;
|
|
22
22
|
readonly lemma?: string;
|
|
23
|
+
/** 書いた形の読み(日本語はカタカナ)。表記の違う同じ語(下さい・ください)を同じと見るため。読めない adapter は持たない。 */
|
|
24
|
+
readonly reading?: string;
|
|
23
25
|
readonly features?: Readonly<Record<string, string>>;
|
|
24
26
|
};
|
|
25
27
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Paragraph } from "./plugin.ts";
|
|
2
|
+
import { isClosed } from "./sentence-shape.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 本題の前にあって、読み手が読み通す段落。文が一つも終止符で閉じない段落は数えない。
|
|
6
|
+
* 分類の札(「エネルギー・環境」)、著者と所属の行、「印刷」、テンプレートの指示は文ではなく、読ませる前置きではない。
|
|
7
|
+
*/
|
|
8
|
+
export const preambleParagraphs = (paragraphs: readonly Paragraph[], bodyStart: number): Paragraph[] =>
|
|
9
|
+
paragraphs.filter((paragraph) => paragraph.span.start < bodyStart && paragraph.sentences.some(isClosed));
|
package/src/structure/build.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { DocumentProfile, Mention, NumberedLine, NumberingContext, Span, St
|
|
|
8
8
|
import { lineNumberAt, linesOf, type Line } from "./lines.ts";
|
|
9
9
|
import { dottedNumber } from "./universal.ts";
|
|
10
10
|
import { unnumberedUnit, type OpenUnit } from "./unnumbered.ts";
|
|
11
|
+
import { readsOnHeading } from "./heading-leaves.ts";
|
|
11
12
|
|
|
12
13
|
/** 組み立て中の節点。できあがったら StructureNode に固める。 */
|
|
13
14
|
type Draft = {
|
|
@@ -179,17 +180,19 @@ const scopeOf = (state: State, patterns: StructurePatterns, text: string): Reado
|
|
|
179
180
|
return within === undefined ? { scope: "local" } : { scope: "local", within: within.address };
|
|
180
181
|
};
|
|
181
182
|
|
|
182
|
-
const addLeaves = (state: State, patterns: StructurePatterns, line: Line, text: string, offset: number): void => {
|
|
183
|
+
const addLeaves = (state: State, patterns: StructurePatterns, line: Line, text: string, offset: number, onHeading: boolean): void => {
|
|
183
184
|
const parent = top(state).draft;
|
|
184
185
|
const scope = scopeOf(state, patterns, text);
|
|
185
|
-
const leaves = LEAVES.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
186
|
+
const leaves = LEAVES.filter(({ kind }) => !onHeading || readsOnHeading(kind))
|
|
187
|
+
.flatMap(({ kind, find }) =>
|
|
188
|
+
find(patterns, text, state.profile).map((mention) => ({
|
|
189
|
+
kind,
|
|
190
|
+
start: line.start + offset + mention.start,
|
|
191
|
+
end: line.start + offset + mention.end,
|
|
192
|
+
attrs: mention.attrs,
|
|
193
|
+
})),
|
|
194
|
+
)
|
|
195
|
+
.sort((left, right) => left.start - right.start);
|
|
193
196
|
leaves.forEach((leaf) =>
|
|
194
197
|
parent.children.push({
|
|
195
198
|
kind: leaf.kind,
|
|
@@ -303,7 +306,7 @@ const readLine = (state: State, patterns: StructurePatterns, line: Line, heading
|
|
|
303
306
|
// 番号付きの行は番号の後ろだけを読む。「第3条(支払)」の「第3条」を自分への参照として拾わない。
|
|
304
307
|
const scanned = numbered === undefined ? text : numbered.rest;
|
|
305
308
|
const offset = Math.max(0, line.text.lastIndexOf(scanned));
|
|
306
|
-
if (scanned !== "") addLeaves(state, patterns, line, scanned, offset);
|
|
309
|
+
if (scanned !== "") addLeaves(state, patterns, line, scanned, offset, heading !== undefined);
|
|
307
310
|
};
|
|
308
311
|
|
|
309
312
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StructureKind } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 見出しの行から拾う葉か。見出しは節の題で、「『特別警報』とは」は下の本文が定義する語を掲げているだけで、定義ではない。
|
|
5
|
+
* 定義として数えると、本文の「『特別警報』とは、…」が二度目の定義に見える。参照や数量は見出しにあっても読む。
|
|
6
|
+
*/
|
|
7
|
+
export const readsOnHeading = (kind: StructureKind): boolean => kind !== "definition";
|