chaffjs 0.14.0 → 0.15.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/address-chain.d.ts +4 -1
- package/dist/address-chain.d.ts.map +1 -1
- package/dist/address-chain.js +5 -2
- package/dist/address-chain.js.map +1 -1
- package/dist/cli-text.d.ts +4 -1
- package/dist/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +14 -2
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +21 -21
- package/dist/cli.js.map +1 -1
- package/dist/commands/eval.d.ts +4 -0
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +20 -6
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/feedback.d.ts.map +1 -1
- package/dist/commands/feedback.js +2 -1
- package/dist/commands/feedback.js.map +1 -1
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +3 -2
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +2 -1
- package/dist/commands/tree.js.map +1 -1
- package/dist/detectors/acronym-compound.d.ts +8 -0
- package/dist/detectors/acronym-compound.d.ts.map +1 -0
- package/dist/detectors/acronym-compound.js +8 -0
- package/dist/detectors/acronym-compound.js.map +1 -0
- package/dist/detectors/acronym-context.d.ts +4 -0
- package/dist/detectors/acronym-context.d.ts.map +1 -1
- package/dist/detectors/acronym-context.js +17 -1
- package/dist/detectors/acronym-context.js.map +1 -1
- package/dist/detectors/acronym-expansion.d.ts +10 -0
- package/dist/detectors/acronym-expansion.d.ts.map +1 -1
- package/dist/detectors/acronym-expansion.js +63 -5
- package/dist/detectors/acronym-expansion.js.map +1 -1
- package/dist/detectors/char-shape.d.ts.map +1 -1
- package/dist/detectors/char-shape.js +36 -14
- package/dist/detectors/char-shape.js.map +1 -1
- package/dist/detectors/citation-key.d.ts +13 -0
- package/dist/detectors/citation-key.d.ts.map +1 -0
- package/dist/detectors/citation-key.js +9 -0
- package/dist/detectors/citation-key.js.map +1 -0
- package/dist/detectors/cited-name.d.ts +11 -0
- package/dist/detectors/cited-name.d.ts.map +1 -0
- package/dist/detectors/cited-name.js +15 -0
- package/dist/detectors/cited-name.js.map +1 -0
- package/dist/detectors/concrete-evidence.d.ts +7 -0
- package/dist/detectors/concrete-evidence.d.ts.map +1 -0
- package/dist/detectors/concrete-evidence.js +21 -0
- package/dist/detectors/concrete-evidence.js.map +1 -0
- package/dist/detectors/counter-edge.d.ts +8 -0
- package/dist/detectors/counter-edge.d.ts.map +1 -0
- package/dist/detectors/counter-edge.js +27 -0
- package/dist/detectors/counter-edge.js.map +1 -0
- package/dist/detectors/dotted-name.d.ts +13 -0
- package/dist/detectors/dotted-name.d.ts.map +1 -0
- package/dist/detectors/dotted-name.js +10 -0
- package/dist/detectors/dotted-name.js.map +1 -0
- package/dist/detectors/doubled-word.d.ts +6 -1
- package/dist/detectors/doubled-word.d.ts.map +1 -1
- package/dist/detectors/doubled-word.js +22 -2
- package/dist/detectors/doubled-word.js.map +1 -1
- package/dist/detectors/en-shape.d.ts +1 -1
- package/dist/detectors/en-shape.d.ts.map +1 -1
- package/dist/detectors/en-shape.js +11 -22
- package/dist/detectors/en-shape.js.map +1 -1
- package/dist/detectors/gram-place.d.ts +0 -4
- package/dist/detectors/gram-place.d.ts.map +1 -1
- package/dist/detectors/gram-place.js +6 -1
- package/dist/detectors/gram-place.js.map +1 -1
- package/dist/detectors/gram-predicate.d.ts +7 -0
- package/dist/detectors/gram-predicate.d.ts.map +1 -0
- package/dist/detectors/gram-predicate.js +44 -0
- package/dist/detectors/gram-predicate.js.map +1 -0
- package/dist/detectors/heading-case.d.ts +21 -0
- package/dist/detectors/heading-case.d.ts.map +1 -0
- package/dist/detectors/heading-case.js +38 -0
- package/dist/detectors/heading-case.js.map +1 -0
- package/dist/detectors/heading-echo.d.ts.map +1 -1
- package/dist/detectors/heading-echo.js +4 -1
- package/dist/detectors/heading-echo.js.map +1 -1
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +7 -4
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/name-numeral.d.ts +16 -0
- package/dist/detectors/name-numeral.d.ts.map +1 -0
- package/dist/detectors/name-numeral.js +30 -0
- package/dist/detectors/name-numeral.js.map +1 -0
- package/dist/detectors/name-part.d.ts +8 -0
- package/dist/detectors/name-part.d.ts.map +1 -0
- package/dist/detectors/name-part.js +20 -0
- package/dist/detectors/name-part.js.map +1 -0
- package/dist/detectors/name-run.d.ts +8 -0
- package/dist/detectors/name-run.d.ts.map +1 -0
- package/dist/detectors/name-run.js +13 -0
- package/dist/detectors/name-run.js.map +1 -0
- package/dist/detectors/orthography.d.ts.map +1 -1
- package/dist/detectors/orthography.js +15 -5
- package/dist/detectors/orthography.js.map +1 -1
- package/dist/detectors/placeholder.d.ts +14 -0
- package/dist/detectors/placeholder.d.ts.map +1 -0
- package/dist/detectors/placeholder.js +24 -0
- package/dist/detectors/placeholder.js.map +1 -0
- package/dist/detectors/quoted-variant.d.ts +7 -0
- package/dist/detectors/quoted-variant.d.ts.map +1 -0
- package/dist/detectors/quoted-variant.js +18 -0
- package/dist/detectors/quoted-variant.js.map +1 -0
- package/dist/detectors/series-label.d.ts +13 -0
- package/dist/detectors/series-label.d.ts.map +1 -0
- package/dist/detectors/series-label.js +26 -0
- package/dist/detectors/series-label.js.map +1 -0
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +29 -24
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/structure-tree.js +1 -1
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/superlative-name.d.ts +9 -0
- package/dist/detectors/superlative-name.d.ts.map +1 -0
- package/dist/detectors/superlative-name.js +24 -0
- package/dist/detectors/superlative-name.js.map +1 -0
- package/dist/detectors/token-shape.d.ts.map +1 -1
- package/dist/detectors/token-shape.js +10 -2
- package/dist/detectors/token-shape.js.map +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +10 -4
- package/dist/document.js.map +1 -1
- package/dist/genre-check.d.ts +34 -0
- package/dist/genre-check.d.ts.map +1 -0
- package/dist/genre-check.js +21 -0
- package/dist/genre-check.js.map +1 -0
- package/dist/genre.d.ts +8 -1
- package/dist/genre.d.ts.map +1 -1
- package/dist/genre.js +29 -5
- package/dist/genre.js.map +1 -1
- package/dist/init.d.ts +0 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +0 -12
- package/dist/init.js.map +1 -1
- package/dist/joined-view.d.ts +25 -0
- package/dist/joined-view.d.ts.map +1 -0
- package/dist/joined-view.js +76 -0
- package/dist/joined-view.js.map +1 -0
- package/dist/line-continues.d.ts +6 -0
- package/dist/line-continues.d.ts.map +1 -0
- package/dist/line-continues.js +33 -0
- package/dist/line-continues.js.map +1 -0
- package/dist/measure.d.ts +1 -0
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +5 -1
- package/dist/measure.js.map +1 -1
- package/dist/number-name.d.ts +6 -3
- package/dist/number-name.d.ts.map +1 -1
- package/dist/number-name.js +31 -14
- package/dist/number-name.js.map +1 -1
- package/dist/orthography.d.ts +4 -0
- package/dist/orthography.d.ts.map +1 -1
- package/dist/orthography.js +11 -9
- package/dist/orthography.js.map +1 -1
- package/dist/plain-source.d.ts +6 -0
- package/dist/plain-source.d.ts.map +1 -0
- package/dist/plain-source.js +6 -0
- package/dist/plain-source.js.map +1 -0
- package/dist/plugin.d.ts +16 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/quoted-span.d.ts +10 -0
- package/dist/quoted-span.d.ts.map +1 -0
- package/dist/quoted-span.js +20 -0
- package/dist/quoted-span.js.map +1 -0
- package/dist/render/eval.d.ts +2 -1
- package/dist/render/eval.d.ts.map +1 -1
- package/dist/render/eval.js +84 -43
- package/dist/render/eval.js.map +1 -1
- package/dist/resolve-genre.d.ts +11 -0
- package/dist/resolve-genre.d.ts.map +1 -0
- package/dist/resolve-genre.js +18 -0
- package/dist/resolve-genre.js.map +1 -0
- package/dist/soft-break.d.ts +19 -0
- package/dist/soft-break.d.ts.map +1 -0
- package/dist/soft-break.js +78 -0
- package/dist/soft-break.js.map +1 -0
- package/dist/structure/build.d.ts.map +1 -1
- package/dist/structure/build.js +19 -9
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/date-order.d.ts +3 -0
- package/dist/structure/date-order.d.ts.map +1 -1
- package/dist/structure/date-order.js +9 -1
- package/dist/structure/date-order.js.map +1 -1
- package/dist/structure/issues.d.ts +1 -1
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +14 -3
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/listed-tags.d.ts +3 -0
- package/dist/structure/listed-tags.d.ts.map +1 -0
- package/dist/structure/listed-tags.js +6 -0
- package/dist/structure/listed-tags.js.map +1 -0
- package/dist/structure/number-in-sentence.d.ts +15 -0
- package/dist/structure/number-in-sentence.d.ts.map +1 -0
- package/dist/structure/number-in-sentence.js +13 -0
- package/dist/structure/number-in-sentence.js.map +1 -0
- package/dist/structure/unit-word.d.ts +8 -0
- package/dist/structure/unit-word.d.ts.map +1 -0
- package/dist/structure/unit-word.js +10 -0
- package/dist/structure/unit-word.js.map +1 -0
- package/dist/structure/wrapped-tail.d.ts +18 -0
- package/dist/structure/wrapped-tail.d.ts.map +1 -0
- package/dist/structure/wrapped-tail.js +11 -0
- package/dist/structure/wrapped-tail.js.map +1 -0
- package/package.json +3 -3
- package/rules/dangling-reference.yaml +2 -2
- package/rules/date-order.yaml +2 -2
- package/rules/latin-spacing.yaml +4 -2
- package/rules/max-kanji-continuous.yaml +1 -1
- package/rules/no-doubled-joshi.yaml +2 -2
- package/rules/title-case-consistency.yaml +2 -2
- package/rules/undefined-acronym.yaml +14 -1
- package/rules/unqualified-superlative.yaml +1 -1
- package/src/address-chain.ts +5 -2
- package/src/cli-text.ts +19 -3
- package/src/cli.ts +22 -19
- package/src/commands/eval.ts +26 -5
- package/src/commands/feedback.ts +2 -1
- package/src/commands/test.ts +3 -2
- package/src/commands/tree.ts +2 -1
- package/src/detectors/acronym-compound.ts +9 -0
- package/src/detectors/acronym-context.ts +22 -1
- package/src/detectors/acronym-expansion.ts +72 -5
- package/src/detectors/char-shape.ts +42 -14
- package/src/detectors/citation-key.ts +13 -0
- package/src/detectors/cited-name.ts +23 -0
- package/src/detectors/concrete-evidence.ts +31 -0
- package/src/detectors/counter-edge.ts +29 -0
- package/src/detectors/dotted-name.ts +14 -0
- package/src/detectors/doubled-word.ts +25 -2
- package/src/detectors/en-shape.ts +17 -30
- package/src/detectors/gram-place.ts +8 -1
- package/src/detectors/gram-predicate.ts +55 -0
- package/src/detectors/heading-case.ts +41 -0
- package/src/detectors/heading-echo.ts +5 -1
- package/src/detectors/lexicon.ts +8 -5
- package/src/detectors/name-numeral.ts +37 -0
- package/src/detectors/name-part.ts +24 -0
- package/src/detectors/name-run.ts +16 -0
- package/src/detectors/orthography.ts +17 -8
- package/src/detectors/placeholder.ts +38 -0
- package/src/detectors/quoted-variant.ts +20 -0
- package/src/detectors/series-label.ts +33 -0
- package/src/detectors/signals.ts +35 -30
- package/src/detectors/structure-tree.ts +2 -2
- package/src/detectors/superlative-name.ts +30 -0
- package/src/detectors/token-shape.ts +12 -3
- package/src/document.ts +12 -5
- package/src/genre-check.ts +46 -0
- package/src/genre.ts +34 -5
- package/src/init.ts +0 -13
- package/src/joined-view.ts +96 -0
- package/src/line-continues.ts +43 -0
- package/src/measure.ts +9 -1
- package/src/number-name.ts +35 -13
- package/src/orthography.ts +15 -9
- package/src/plain-source.ts +5 -0
- package/src/plugin.ts +21 -2
- package/src/quoted-span.ts +25 -0
- package/src/render/eval.ts +101 -38
- package/src/resolve-genre.ts +20 -0
- package/src/soft-break.ts +94 -0
- package/src/structure/build.ts +19 -10
- package/src/structure/date-order.ts +16 -1
- package/src/structure/issues.ts +18 -3
- package/src/structure/listed-tags.ts +7 -0
- package/src/structure/number-in-sentence.ts +20 -0
- package/src/structure/unit-word.ts +11 -0
- package/src/structure/wrapped-tail.ts +22 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Span } from "./plugin.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 段落の中の改行のうち、読み手には見えないもの。
|
|
5
|
+
*
|
|
6
|
+
* Markdown は段落の中の改行を空白として表示するが、全角の文字に挟まれた改行は空白にもならずに消える
|
|
7
|
+
* (CSS Text 3 の segment break transformation。ブラウザがそう描く)。日本語は語の間に空白を置かないので、
|
|
8
|
+
* PDF から写した判決や、行の途中で折り返して書いた段落をそのまま解析器に渡すと、改行が語を割る(「関」「する」)。
|
|
9
|
+
* 英字・数字・ハングルに接する改行は、読み手にも空白に見えるので残す。
|
|
10
|
+
*/
|
|
11
|
+
const WIDE = /[\p{scx=Han}\p{scx=Hiragana}\p{scx=Katakana}、-〿!-⦆¢-₩]/u;
|
|
12
|
+
|
|
13
|
+
const NEWLINE = /\n/gu;
|
|
14
|
+
const isHorizontalSpace = (unit: string | undefined): boolean => unit === " " || unit === "\t";
|
|
15
|
+
|
|
16
|
+
/** 改行の前に空白が 2 つ以上あれば、Markdown の強制改行(hard line break)。読み手にも改行に見える。 */
|
|
17
|
+
const HARD_BREAK_SPACES = 2;
|
|
18
|
+
|
|
19
|
+
const isLowSurrogate = (unit: number): boolean => unit >= 0xdc00 && unit <= 0xdfff;
|
|
20
|
+
|
|
21
|
+
/** index の直前の 1 文字(サロゲートペアは 2 単位で 1 文字)。 */
|
|
22
|
+
const charBefore = (text: string, index: number): string => {
|
|
23
|
+
const width = index >= 2 && isLowSurrogate(text.charCodeAt(index - 1)) ? 2 : 1;
|
|
24
|
+
return text.slice(Math.max(0, index - width), index);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const charAt = (text: string, index: number): string => {
|
|
28
|
+
const point = text.codePointAt(index);
|
|
29
|
+
return point === undefined ? "" : String.fromCodePoint(point);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** 改行を挟む 2 文字が、改行を消して読む組か。 */
|
|
33
|
+
export const joinsAcrossBreak = (before: string, after: string): boolean => WIDE.test(before) && WIDE.test(after);
|
|
34
|
+
|
|
35
|
+
/** from から back の向きに、空白(スペース・タブ)が続く先。 */
|
|
36
|
+
const spacesUntil = (text: string, from: number, step: 1 | -1): number => {
|
|
37
|
+
let at = from;
|
|
38
|
+
while (isHorizontalSpace(text[step === 1 ? at : at - 1])) at += step;
|
|
39
|
+
return at;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** 改行 1 つと、その前の行末・後ろの行頭の空白。正規表現で取ると、空白の長い並びで後戻りが増える。 */
|
|
43
|
+
const lineBreakAt = (text: string, newline: number): { readonly span: Span; readonly trailingSpaces: number } => {
|
|
44
|
+
const lineEnd = text[newline - 1] === "\r" ? newline - 1 : newline;
|
|
45
|
+
const start = spacesUntil(text, lineEnd, -1);
|
|
46
|
+
return { span: { start, end: spacesUntil(text, newline + 1, 1) }, trailingSpaces: lineEnd - start };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const isSoft = (text: string, found: { readonly span: Span; readonly trailingSpaces: number }): boolean =>
|
|
50
|
+
found.trailingSpaces < HARD_BREAK_SPACES && joinsAcrossBreak(charBefore(text, found.span.start), charAt(text, found.span.end));
|
|
51
|
+
|
|
52
|
+
/** text の中の、消える改行(前後の行頭・行末の空白ごと)。text の文字だけで決める。 */
|
|
53
|
+
export const softBreaks = (text: string): Span[] =>
|
|
54
|
+
[...text.matchAll(NEWLINE)]
|
|
55
|
+
.map((match) => lineBreakAt(text, match.index))
|
|
56
|
+
.filter((found) => isSoft(text, found))
|
|
57
|
+
.map((found) => found.span);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 覆った文字(コード・リンクの記号・URL)に触れない、消える改行。source と prose は同じ長さで、同じ位置に同じ文字がある。
|
|
61
|
+
*
|
|
62
|
+
* 覆った文字は空白になっているので、prose だけでは行頭のインデントとインラインコードの跡を見分けられない。
|
|
63
|
+
* 見分けずにつなぐと、行頭の `npm` の前後の語が 1 語になる。
|
|
64
|
+
*/
|
|
65
|
+
export const unmaskedSoftBreaks = (source: string, prose: string): Span[] =>
|
|
66
|
+
softBreaks(prose).filter((span) => source.slice(span.start, span.end) === prose.slice(span.start, span.end));
|
|
67
|
+
|
|
68
|
+
/** breaks(text の中の範囲、昇順・重ならない)を取り除いた文字列。 */
|
|
69
|
+
export const withoutSpans = (text: string, breaks: readonly Span[]): string => {
|
|
70
|
+
const parts: string[] = [];
|
|
71
|
+
const cursor = breaks.reduce((from, span) => {
|
|
72
|
+
parts.push(text.slice(from, span.start));
|
|
73
|
+
return span.end;
|
|
74
|
+
}, 0);
|
|
75
|
+
parts.push(text.slice(cursor));
|
|
76
|
+
return parts.join("");
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** sorted(昇順・重ならない)の中で、end が offset より後ろの最初の添字。無ければ sorted.length。 */
|
|
80
|
+
export const firstEndingAfter = (sorted: readonly Span[], offset: number): number => {
|
|
81
|
+
const search = (low: number, high: number): number => {
|
|
82
|
+
if (low >= high) return low;
|
|
83
|
+
const middle = (low + high) >> 1;
|
|
84
|
+
return (sorted[middle]?.end ?? Number.POSITIVE_INFINITY) > offset ? search(low, middle) : search(middle + 1, high);
|
|
85
|
+
};
|
|
86
|
+
return search(0, sorted.length);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** sorted(昇順・重ならない)のうち、outer に収まるもの。段落や文ごとに全部をなめない。 */
|
|
90
|
+
export const spansWithin = <T extends Span>(sorted: readonly T[], outer: Span): T[] => {
|
|
91
|
+
const from = firstEndingAfter(sorted, outer.start);
|
|
92
|
+
const to = firstEndingAfter(sorted, outer.end);
|
|
93
|
+
return sorted.slice(from, to).filter((span) => span.start >= outer.start);
|
|
94
|
+
};
|
package/src/structure/build.ts
CHANGED
|
@@ -7,8 +7,10 @@ import { maskSpans } from "../mask.ts";
|
|
|
7
7
|
import type { DocumentProfile, Mention, NumberedLine, NumberingContext, Span, StructureKind, StructureNode, StructurePatterns } from "../plugin.ts";
|
|
8
8
|
import { lineNumberAt, linesOf, type Line } from "./lines.ts";
|
|
9
9
|
import { dottedNumber } from "./universal.ts";
|
|
10
|
+
import { numberInSentence } from "./number-in-sentence.ts";
|
|
10
11
|
import { unnumberedUnit, type OpenUnit } from "./unnumbered.ts";
|
|
11
12
|
import { readsOnHeading } from "./heading-leaves.ts";
|
|
13
|
+
import { onLine, wrappedLine, type WrappedLine } from "./wrapped-tail.ts";
|
|
12
14
|
|
|
13
15
|
/** 組み立て中の節点。できあがったら StructureNode に固める。 */
|
|
14
16
|
type Draft = {
|
|
@@ -129,11 +131,14 @@ const outsideAddresses = (mentions: readonly Mention[], text: string, profile: D
|
|
|
129
131
|
const withoutAbsolute = (relative: readonly Mention[], absolute: readonly Mention[]): readonly Mention[] =>
|
|
130
132
|
relative.filter((mention) => !absolute.some((other) => mention.start < other.end && other.start < mention.end));
|
|
131
133
|
|
|
132
|
-
type LeafFinder = (patterns: StructurePatterns, text: string, profile: DocumentProfile | undefined) => readonly Mention[];
|
|
134
|
+
type LeafFinder = (patterns: StructurePatterns, text: string, profile: DocumentProfile | undefined, wrapped: WrappedLine) => readonly Mention[];
|
|
133
135
|
|
|
134
136
|
const LEAVES: readonly { readonly kind: StructureKind; readonly find: LeafFinder }[] = [
|
|
135
137
|
{ kind: "definition", find: (patterns, text) => patterns.definitions(text) },
|
|
136
|
-
{
|
|
138
|
+
{
|
|
139
|
+
kind: "reference",
|
|
140
|
+
find: (patterns, _text, profile, wrapped) => onLine(substitutedDocuments(patterns.references(wrapped.text), wrapped.text, profile), wrapped),
|
|
141
|
+
},
|
|
137
142
|
{
|
|
138
143
|
kind: "reference",
|
|
139
144
|
find: (patterns, text, profile) => {
|
|
@@ -146,10 +151,12 @@ const LEAVES: readonly { readonly kind: StructureKind; readonly find: LeafFinder
|
|
|
146
151
|
{ kind: "date", find: (patterns, text) => patterns.dates?.(text) ?? [] },
|
|
147
152
|
];
|
|
148
153
|
|
|
149
|
-
/**
|
|
154
|
+
/** 言語を問わない通し番号。後ろが単位なら数量、本文の文の続きなら文の中の数なので、番号にしない。 */
|
|
150
155
|
const universalNumber = (patterns: StructurePatterns, text: string, context: NumberingContext, plainText: boolean): NumberedLine | undefined => {
|
|
151
156
|
const dotted = dottedNumber(text, context, plainText);
|
|
152
|
-
|
|
157
|
+
if (dotted === undefined || patterns.countedAfter?.(dotted.number, dotted.rest) === true) return undefined;
|
|
158
|
+
const numbered = { number: dotted.number, rest: dotted.rest, isHeading: context.isHeading };
|
|
159
|
+
return numberInSentence(numbered, patterns.continuesSentence) ? undefined : dotted;
|
|
153
160
|
};
|
|
154
161
|
|
|
155
162
|
/** 行の中の定義・参照・義務・数量を、いま開いている最も内側の節点の子にする。 */
|
|
@@ -180,12 +187,13 @@ const scopeOf = (state: State, patterns: StructurePatterns, text: string): Reado
|
|
|
180
187
|
return within === undefined ? { scope: "local" } : { scope: "local", within: within.address };
|
|
181
188
|
};
|
|
182
189
|
|
|
183
|
-
const addLeaves = (state: State, patterns: StructurePatterns, line: Line,
|
|
190
|
+
const addLeaves = (state: State, patterns: StructurePatterns, line: Line, wrapped: WrappedLine, offset: number, onHeading: boolean): void => {
|
|
184
191
|
const parent = top(state).draft;
|
|
192
|
+
const text = wrapped.line;
|
|
185
193
|
const scope = scopeOf(state, patterns, text);
|
|
186
194
|
const leaves = LEAVES.filter(({ kind }) => !onHeading || readsOnHeading(kind))
|
|
187
195
|
.flatMap(({ kind, find }) =>
|
|
188
|
-
find(patterns, text, state.profile).map((mention) => ({
|
|
196
|
+
find(patterns, text, state.profile, wrapped).map((mention) => ({
|
|
189
197
|
kind,
|
|
190
198
|
start: line.start + offset + mention.start,
|
|
191
199
|
end: line.start + offset + mention.end,
|
|
@@ -290,7 +298,7 @@ const withCaption = (state: State, line: Line, numbered: NumberedLine | undefine
|
|
|
290
298
|
return numbered === undefined || caption === undefined ? numbered : { ...numbered, heading: caption };
|
|
291
299
|
};
|
|
292
300
|
|
|
293
|
-
const readLine = (state: State, patterns: StructurePatterns, line: Line, heading: Heading | undefined): void => {
|
|
301
|
+
const readLine = (state: State, patterns: StructurePatterns, line: Line, heading: Heading | undefined, next: string | undefined): void => {
|
|
294
302
|
const text = heading === undefined ? line.text : headingLineText(line.text);
|
|
295
303
|
const openNumbers = state.stack.flatMap((frame) => (frame.numbered === undefined ? [] : [frame.numbered]));
|
|
296
304
|
const context = { open: openNumbers, isHeading: heading !== undefined };
|
|
@@ -306,7 +314,8 @@ const readLine = (state: State, patterns: StructurePatterns, line: Line, heading
|
|
|
306
314
|
// 番号付きの行は番号の後ろだけを読む。「第3条(支払)」の「第3条」を自分への参照として拾わない。
|
|
307
315
|
const scanned = numbered === undefined ? text : numbered.rest;
|
|
308
316
|
const offset = Math.max(0, line.text.lastIndexOf(scanned));
|
|
309
|
-
|
|
317
|
+
const onHeading = heading !== undefined;
|
|
318
|
+
if (scanned !== "") addLeaves(state, patterns, line, wrappedLine(scanned, onHeading, next), offset, onHeading);
|
|
310
319
|
};
|
|
311
320
|
|
|
312
321
|
/**
|
|
@@ -331,8 +340,8 @@ export const buildTree = (input: StructureInput, patterns: StructurePatterns): S
|
|
|
331
340
|
captions: new Map(),
|
|
332
341
|
plainText: !input.markdown,
|
|
333
342
|
};
|
|
334
|
-
lines.forEach((line) => {
|
|
335
|
-
if (line.text.trim() !== "") readLine(state, patterns, line, headings.get(line.number));
|
|
343
|
+
lines.forEach((line, index) => {
|
|
344
|
+
if (line.text.trim() !== "") readLine(state, patterns, line, headings.get(line.number), lines[index + 1]?.text);
|
|
336
345
|
// コードの行は覆って読まないが、開いている節の中身ではある。節の最後にコードブロックがあっても、範囲をそこまで伸ばす。
|
|
337
346
|
else if (input.source.slice(line.start, line.start + line.text.length).trim() !== "") extend(state, line.start + line.text.length);
|
|
338
347
|
});
|
|
@@ -5,6 +5,7 @@ import { runsOf, type Line } from "./runs.ts";
|
|
|
5
5
|
* 日程として並べた日付の順番。箇条書きの続いた項目か、表の続いた行で、日付をちょうど一つ持つものを並びとして読む。
|
|
6
6
|
* 並びの向き(古い順か新しい順か)は多いほうで決め、それに逆らう一歩だけを言う。新しい順に並べた履歴は正しい並び。
|
|
7
7
|
* 向きが決まらない並び(上がりと下がりが同じ数)は何も言わない。逆らう一歩を言えるのは、日付が 4 つ以上の並びだけになる。
|
|
8
|
+
* 向きに沿って並ぶ日付が半分以下の並びは、日付でなく別のもの(名前、版)で並べた一覧として何も言わない。
|
|
8
9
|
*/
|
|
9
10
|
export type DatedPoint = { readonly offset: number; readonly value: string };
|
|
10
11
|
|
|
@@ -23,6 +24,19 @@ const samePrecision = (dated: readonly DatedPoint[]): boolean => PRECISIONS.some
|
|
|
23
24
|
/** 上がりが多ければ 1、下がりが多ければ -1、同じ数なら 0(向きを決めない)。 */
|
|
24
25
|
const majorityOf = (up: number, down: number): number => Math.sign(up - down);
|
|
25
26
|
|
|
27
|
+
/** 向きに沿って並べられる日付の最大の数(間を飛ばしてよい。等しい日付は向きに逆らわない)。 */
|
|
28
|
+
export const longestInOrder = (values: readonly string[], direction: number): number =>
|
|
29
|
+
Math.max(
|
|
30
|
+
0,
|
|
31
|
+
...values.reduce<number[]>((lengths, value, index) => {
|
|
32
|
+
const along = values.slice(0, index).map((earlier, at) => (Math.sign(value.localeCompare(earlier)) === -direction ? 0 : (lengths[at] ?? 0)));
|
|
33
|
+
return [...lengths, 1 + Math.max(0, ...along)];
|
|
34
|
+
}, []),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/** 日付のうち向きに沿うものが過半を占める。ひとつふたつの書き間違いなら残りが揃うが、名前順の一覧は揃わない。 */
|
|
38
|
+
const mostlyInOrder = (values: readonly string[], direction: number): boolean => longestInOrder(values, direction) * 2 > values.length;
|
|
39
|
+
|
|
26
40
|
/** 多いほうの向きに逆らう一歩。後ろの項目の日付を指す。 */
|
|
27
41
|
const againstMajority = (dated: readonly DatedPoint[]): StructureIssue[] => {
|
|
28
42
|
const steps = dated
|
|
@@ -31,7 +45,8 @@ const againstMajority = (dated: readonly DatedPoint[]): StructureIssue[] => {
|
|
|
31
45
|
const up = steps.filter((step) => step.sign > 0).length;
|
|
32
46
|
const down = steps.filter((step) => step.sign < 0).length;
|
|
33
47
|
const majority = majorityOf(up, down);
|
|
34
|
-
|
|
48
|
+
const values = dated.map((point) => point.value);
|
|
49
|
+
if (majority === 0 || !mostlyInOrder(values, majority)) return [];
|
|
35
50
|
return steps
|
|
36
51
|
.filter((step) => step.sign === -majority)
|
|
37
52
|
.map((step) => ({ offset: step.point.offset, values: { date: step.point.value, previous: step.previous?.value ?? "" } }));
|
package/src/structure/issues.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { StructureNode } from "../plugin.ts";
|
|
2
|
+
import { listedTags } from "./listed-tags.ts";
|
|
3
|
+
import { namesAbsentUnit } from "./unit-word.ts";
|
|
2
4
|
|
|
3
5
|
// 木だけで決まる誤り。参照先が無い、番号が飛ぶ・重なる、同じ語を二度定義する。
|
|
4
6
|
// 意味の食い違いは判定しない。書いてあることから機械で決まるものだけを出す。
|
|
@@ -46,24 +48,37 @@ const addressesOf = (node: StructureNode): string[] =>
|
|
|
46
48
|
|
|
47
49
|
const keysOf = (node: StructureNode): string[] => addressesOf(node).flatMap((address) => [address, labelled(address, textOf(node, "label"))]);
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* 他の文書を指す参照か。citedTag("[HTTP-CACHING]")は、書き方だけでは差し込み欄の "[BUYER-1]" と見分けられないので、
|
|
53
|
+
* 文書がその語を一覧に載せているときだけ他の文書の名とみなす。
|
|
54
|
+
*/
|
|
55
|
+
const citesOtherDocument =
|
|
56
|
+
(listed: ReadonlySet<string>) =>
|
|
57
|
+
(node: StructureNode): boolean =>
|
|
58
|
+
node.attrs["document"] !== undefined || (node.attrs["citedTag"] !== undefined && listed.has(textOf(node, "citedTag")));
|
|
59
|
+
|
|
49
60
|
/**
|
|
50
61
|
* 参照の番地が木に無い。他の文書の名前が付いた参照(民法第709条、Section 9 of the Master Agreement)は引かない。
|
|
51
62
|
* 条を 1 つも持たない文書も見ない。他の文書を指しているだけかもしれない。
|
|
52
63
|
*/
|
|
53
|
-
export const danglingReferences = (tree: StructureNode): StructureIssue[] => {
|
|
64
|
+
export const danglingReferences = (tree: StructureNode, source: string): StructureIssue[] => {
|
|
54
65
|
const nodes = inDocumentOrder(tree);
|
|
55
66
|
// 参照は条を指す。条を一つも持たない文書(契約書に付ける承諾書のひな形など)の「契約書第6条」は、別の文書の条。
|
|
56
67
|
if (!nodes.some((node) => node.kind === "article")) return [];
|
|
68
|
+
const tagged = nodes.some((node) => node.attrs["citedTag"] !== undefined);
|
|
69
|
+
const citesOther = citesOtherDocument(tagged ? listedTags(source) : new Set());
|
|
57
70
|
const addresses = new Set(nodes.filter((node) => NUMBERED.includes(node.kind)).flatMap(keysOf));
|
|
58
71
|
// Section で組んだ法令が "Articles 13 to 21 of the UK GDPR" と別の文書の Article を名指ししていれば、名の無い "Article 6(3)" もそちら。
|
|
59
72
|
// 名指しがあれば、名の無い "Article 9" が書き間違いか向こうの条かは区別できないので黙る。名指しが無い文書では報告する。
|
|
60
73
|
const numbering = (node: StructureNode): unknown => node.attrs["numbering"];
|
|
61
74
|
const numberings = new Set(nodes.flatMap((node) => (node.kind === "article" && node.numbering !== undefined ? [node.numbering] : [])));
|
|
62
|
-
const citedElsewhere = new Set(nodes.filter((node) => node.kind === "reference" && node
|
|
75
|
+
const citedElsewhere = new Set(nodes.filter((node) => node.kind === "reference" && citesOther(node)).map(numbering));
|
|
63
76
|
const otherNumbering = (node: StructureNode): boolean =>
|
|
64
77
|
numberings.size > 0 && typeof numbering(node) === "string" && !numberings.has(String(numbering(node))) && citedElsewhere.has(numbering(node));
|
|
78
|
+
const articleLabels = nodes.filter((node) => node.kind === "article").map((node) => textOf(node, "label"));
|
|
65
79
|
return nodes
|
|
66
|
-
.filter((node) => node.kind === "reference" && node
|
|
80
|
+
.filter((node) => node.kind === "reference" && !citesOther(node) && !otherNumbering(node))
|
|
81
|
+
.filter((node) => !namesAbsentUnit(node, articleLabels))
|
|
67
82
|
.filter((node) => !resolves(node, addresses))
|
|
68
83
|
.map((node) => ({ offset: node.span.start, values: { label: textOf(node, "label"), target: textOf(node, "target") } }));
|
|
69
84
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// 角括弧の見出し語を、文書が一覧に載せているか。一覧の項目は行頭の [語] で始まり、その後ろは行末か、
|
|
2
|
+
// 2 つ以上の空白かタブで本文と離れる(ぶら下げの組み方)。行頭の [語] に空白 1 つで文が続くのは本文。
|
|
3
|
+
|
|
4
|
+
const LISTED = /^[ \t]*\[(?<tag>[^[\]\s]{1,60})\](?:[ \t]*\r?$|[ \t]{2,}|\t)/u;
|
|
5
|
+
|
|
6
|
+
/** 文書が一覧の項目として載せている角括弧の見出し語(括弧を除く)。 */
|
|
7
|
+
export const listedTags = (source: string): ReadonlySet<string> => new Set(source.split("\n").flatMap((line) => LISTED.exec(line)?.groups?.["tag"] ?? []));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** 大文字と小文字のある文字で、小文字の語。節の題(「3.1 Scope」「4.2 The Supplier shall」)は小文字で始まらない。 */
|
|
2
|
+
const LOWERCASE_WORD = /^\p{Ll}/u;
|
|
3
|
+
|
|
4
|
+
/** 文の終わり。ピリオドは後ろが空白か行末のとき(「3.x」「e-Tax.nta」の点は文を終えない)。題は文として終わらない。 */
|
|
5
|
+
const SENTENCE_END = /[.!?](?=\s|$)|[。!?]/u;
|
|
6
|
+
|
|
7
|
+
/** 行頭の通し番号の行。rest は番号の後ろ。 */
|
|
8
|
+
export type NumberedText = { readonly number: string; readonly rest: string; readonly isHeading: boolean };
|
|
9
|
+
|
|
10
|
+
type ContinuesSentence = (number: string, rest: string) => boolean;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 本文の行の「3.11.0 was released on 2024-10-17.」「3.11.0 を公開しました。」の番号は、文の主語や目的語で、節の番号ではない。
|
|
14
|
+
* 続きが文として終わり、しかも小文字の語で始まるか、言語パッケージが文の続き(日本語なら助詞)と読んだとき。
|
|
15
|
+
* 小文字の題(「3.1 overview」「4.2.1. about:blank」)と見出しの行は章番号。
|
|
16
|
+
*/
|
|
17
|
+
export const numberInSentence = (numbered: NumberedText, continuesSentence?: ContinuesSentence): boolean =>
|
|
18
|
+
!numbered.isHeading &&
|
|
19
|
+
SENTENCE_END.test(numbered.rest) &&
|
|
20
|
+
(LOWERCASE_WORD.test(numbered.rest) || continuesSentence?.(numbered.number, numbered.rest) === true);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { StructureNode } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 参照が単位の語(attrs.unitWord、日本語の「条」)で番号を名指していて、文書のどの条の番号にもその語が無い。
|
|
5
|
+
* 見出しに「1 目的」と番号を振った指針の「法第2条」は、指針の 2 ではなく、その語で条を数える別の文書(法律)の条。
|
|
6
|
+
* 単位の語を付けない言語・参照(英語の Section)は、ここでは決めない。
|
|
7
|
+
*/
|
|
8
|
+
export const namesAbsentUnit = (reference: StructureNode, articleLabels: readonly string[]): boolean => {
|
|
9
|
+
const unitWord = reference.attrs["unitWord"];
|
|
10
|
+
return typeof unitWord === "string" && unitWord !== "" && !articleLabels.some((label) => label.includes(unitWord));
|
|
11
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Mention } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 参照を読む行と、その後ろにつないだ次の行。
|
|
5
|
+
*
|
|
6
|
+
* 72 桁で折り返す RFC は「Section 16.3.2\n of [HTTP]」のように、参照と「どの文書の」を別の行に書く。行だけを読むと
|
|
7
|
+
* 自分の文書の 16.3.2 に見える。改行と字下げは読み手には空白一つなので、そうつないだ文字列を言語の読み手に渡す。
|
|
8
|
+
* 拾った参照は行の中にあるものだけを残す。次の行の参照は、次の行を読むときに拾う。位置は行の上のまま。
|
|
9
|
+
*/
|
|
10
|
+
export type WrappedLine = { readonly line: string; readonly text: string; readonly lineLength: number };
|
|
11
|
+
|
|
12
|
+
/** 見出しは次の行へ続かない。見出しの行か、次の行が無い・空なら、行だけ。 */
|
|
13
|
+
export const wrappedLine = (text: string, onHeading: boolean, next: string | undefined): WrappedLine => {
|
|
14
|
+
const tail = onHeading ? "" : (next?.trim() ?? "");
|
|
15
|
+
if (tail === "") return { line: text, text, lineLength: text.length };
|
|
16
|
+
const kept = text.trimEnd();
|
|
17
|
+
return { line: text, text: `${kept} ${tail}`, lineLength: kept.length };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** 行の中で終わる参照だけ。つないだ次の行に入り込んだもの(「Sections 1,\n 2」の 2)は捨てる。 */
|
|
21
|
+
export const onLine = (mentions: readonly Mention[], wrapped: WrappedLine): readonly Mention[] =>
|
|
22
|
+
mentions.filter((mention) => mention.end <= wrapped.lineLength);
|