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
package/src/genre.ts
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/** chaff が知っているジャンル。rule の use_for はこの頭の部分で当てる。 */
|
|
2
|
+
export const GENRES: readonly string[] = [
|
|
3
|
+
"technical/spec",
|
|
4
|
+
"technical/readme",
|
|
5
|
+
"blog/tech",
|
|
6
|
+
"blog/essay",
|
|
7
|
+
"blog/owned-media",
|
|
8
|
+
"business/proposal",
|
|
9
|
+
"business/report",
|
|
10
|
+
"business/email",
|
|
11
|
+
"business/press-release",
|
|
12
|
+
"business/meeting-notes",
|
|
13
|
+
];
|
|
14
|
+
|
|
1
15
|
export type GenreGuess = { readonly genre: string; readonly from: "front-matter" | "path" | "content" };
|
|
2
16
|
|
|
3
17
|
/**
|
|
@@ -35,9 +49,24 @@ export const guessGenre = (path: string, source: string, front: string | undefin
|
|
|
35
49
|
return undefined;
|
|
36
50
|
};
|
|
37
51
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
return
|
|
52
|
+
/** YAML の 1 行の値。行末のコメントと、値を囲む引用符を外す。 */
|
|
53
|
+
const scalarOf = (raw: string): string => {
|
|
54
|
+
const comment = raw.search(/\s#/u);
|
|
55
|
+
const value = (comment === -1 ? raw : raw.slice(0, comment)).trim();
|
|
56
|
+
return /^(["'])(.*)\1$/u.exec(value)?.[2] ?? value;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const valuesOf = (block: string, key: string): string[] => [...block.matchAll(new RegExp(`^${key}:(.*)$`, "gmu"))].map((line) => scalarOf(line[1] ?? ""));
|
|
60
|
+
|
|
61
|
+
/** front matter の 1 つの欄に書かれた値。front matter が無ければ空。 */
|
|
62
|
+
export const frontMatterValues = (source: string, key: string): string[] => {
|
|
63
|
+
const block = /^---\n([\s\S]*?)\n---/u.exec(source)?.[1];
|
|
64
|
+
return block === undefined ? [] : valuesOf(block, key);
|
|
43
65
|
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* front matter の genre / type を拾う。失敗しても落とさない。genre が type より先。
|
|
69
|
+
* 知っているジャンルだけを返す。Zenn の type(tech / idea)のような別の意味の値をジャンルにすると、どの rule も当てはまらず「指摘なし」になる。
|
|
70
|
+
*/
|
|
71
|
+
export const frontMatterGenre = (source: string): string | undefined =>
|
|
72
|
+
[...frontMatterValues(source, "genre"), ...frontMatterValues(source, "type")].find((value) => GENRES.includes(value));
|
package/src/init.ts
CHANGED
|
@@ -125,16 +125,3 @@ export const runInit = (dir: string, genre: string, ui: UiLanguage = "ja"): stri
|
|
|
125
125
|
"",
|
|
126
126
|
];
|
|
127
127
|
};
|
|
128
|
-
|
|
129
|
-
export const GENRES: readonly string[] = [
|
|
130
|
-
"technical/spec",
|
|
131
|
-
"technical/readme",
|
|
132
|
-
"blog/tech",
|
|
133
|
-
"blog/essay",
|
|
134
|
-
"blog/owned-media",
|
|
135
|
-
"business/proposal",
|
|
136
|
-
"business/report",
|
|
137
|
-
"business/email",
|
|
138
|
-
"business/press-release",
|
|
139
|
-
"business/meeting-notes",
|
|
140
|
-
];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spansWithin, withoutSpans } from "./soft-break.ts";
|
|
2
|
+
import { continuedBreaks, wrapBreaks } from "./line-continues.ts";
|
|
3
|
+
import type { Segmentation, Sentence, Span, Token } from "./plugin.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 消える改行を取り除いた文字列と、その上の範囲を元の文字列の範囲に戻す写像。
|
|
7
|
+
*
|
|
8
|
+
* 解析はつないだ文字列で行い、指摘の位置は元の文字列で出す。行・列も、引用して見せる範囲も、元の文書のまま。
|
|
9
|
+
*/
|
|
10
|
+
export type JoinedView = { readonly text: string; readonly toSource: (span: Span) => Span };
|
|
11
|
+
|
|
12
|
+
type Removed = { readonly at: number; readonly removedBefore: number };
|
|
13
|
+
|
|
14
|
+
/** 取り除いた各範囲が、つないだ文字列のどこにあったか。removedBefore はそこまでに取り除いた長さ(その範囲を含む)。 */
|
|
15
|
+
const removedPlaces = (breaks: readonly Span[]): Removed[] => {
|
|
16
|
+
const places: Removed[] = [];
|
|
17
|
+
breaks.reduce((before, span) => {
|
|
18
|
+
const removedBefore = before + span.end - span.start;
|
|
19
|
+
places.push({ at: span.start - before, removedBefore });
|
|
20
|
+
return removedBefore;
|
|
21
|
+
}, 0);
|
|
22
|
+
return places;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** at が offset 以下(inclusive)または未満の、最後の範囲までに取り除いた長さ。places は at の昇順。 */
|
|
26
|
+
const removedUpTo = (places: readonly Removed[], offset: number, inclusive: boolean): number => {
|
|
27
|
+
const search = (low: number, high: number, found: number): number => {
|
|
28
|
+
if (low > high) return found;
|
|
29
|
+
const middle = (low + high) >> 1;
|
|
30
|
+
const place = places[middle];
|
|
31
|
+
if (place === undefined) return found;
|
|
32
|
+
const reached = inclusive ? place.at <= offset : place.at < offset;
|
|
33
|
+
return reached ? search(middle + 1, high, place.removedBefore) : search(low, middle - 1, found);
|
|
34
|
+
};
|
|
35
|
+
return search(0, places.length - 1, 0);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 始まりは、そこで取り除いた改行の後ろ(次の文字)へ。終わりは、そこで取り除いた改行の手前へ。
|
|
40
|
+
* 改行をまたぐ語(「関\nする」)は、改行ごと覆う範囲になる。
|
|
41
|
+
*/
|
|
42
|
+
export const joinedView = (text: string, breaks: readonly Span[]): JoinedView => {
|
|
43
|
+
const places = removedPlaces(breaks);
|
|
44
|
+
const toSource = (span: Span): Span => {
|
|
45
|
+
const start = span.start + removedUpTo(places, span.start, true);
|
|
46
|
+
const end = span.end + removedUpTo(places, span.end, false);
|
|
47
|
+
return { start, end: Math.max(start, end) };
|
|
48
|
+
};
|
|
49
|
+
return { text: withoutSpans(text, breaks), toSource };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const tokenInSource =
|
|
53
|
+
(view: JoinedView) =>
|
|
54
|
+
(token: Token): Token => ({ ...token, span: view.toSource(token.span) });
|
|
55
|
+
|
|
56
|
+
const sentenceInSource =
|
|
57
|
+
(text: string, view: JoinedView) =>
|
|
58
|
+
(sentence: Sentence): Sentence => {
|
|
59
|
+
const span = view.toSource(sentence.span);
|
|
60
|
+
return {
|
|
61
|
+
span,
|
|
62
|
+
text: text.slice(span.start, span.end),
|
|
63
|
+
...(sentence.tokens === undefined ? {} : { tokens: sentence.tokens.map(tokenInSource(view)) }),
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type Segment = (text: string) => Segmentation;
|
|
68
|
+
|
|
69
|
+
/** breaks を除いて segment し、文と語の範囲を text の上に戻す。文の text は元の文字列の切り出しのまま。 */
|
|
70
|
+
const segmentWithout = (text: string, breaks: readonly Span[], segment: Segment): Sentence[] => {
|
|
71
|
+
const view = joinedView(text, breaks);
|
|
72
|
+
return segment(view.text).sentences.map(sentenceInSource(text, view));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const withWrapBreaks = (sentence: Sentence, wrapped: readonly Span[]): Sentence => {
|
|
76
|
+
const inside = spansWithin(wrapped, sentence.span);
|
|
77
|
+
return inside.length === 0 ? sentence : { ...sentence, wrapBreaks: inside };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const tokensOf = (sentences: readonly Sentence[]): Token[] => sentences.flatMap((sentence) => sentence.tokens ?? []);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 消える改行を、行が続いていると言えるものだけ除いて segment する。
|
|
84
|
+
* 一度すべて除いて解析し、その語で決めて、残す改行があれば読み直す。
|
|
85
|
+
* 語が無ければ(品詞を読まない adapter)決められないので、前と同じく改行ごと渡す。
|
|
86
|
+
* 除く改行が無ければ segment の結果をそのまま返す。つながない段落の文は、前と同じものになる。
|
|
87
|
+
*/
|
|
88
|
+
export const segmentJoined = (text: string, breaks: readonly Span[], segment: Segment): readonly Sentence[] => {
|
|
89
|
+
if (breaks.length === 0) return segment(text).sentences;
|
|
90
|
+
const joined = segmentWithout(text, breaks, segment);
|
|
91
|
+
const kept = continuedBreaks(breaks, tokensOf(joined));
|
|
92
|
+
if (kept.length === 0) return segment(text).sentences;
|
|
93
|
+
const sentences = kept.length === breaks.length ? joined : segmentWithout(text, kept, segment);
|
|
94
|
+
const wrapped = wrapBreaks(kept, tokensOf(sentences));
|
|
95
|
+
return sentences.map((sentence) => withWrapBreaks(sentence, wrapped));
|
|
96
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { firstEndingAfter } from "./soft-break.ts";
|
|
2
|
+
import type { Span, Token } from "./plugin.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 消える改行のうち、どれを消して読むか。決めるのは解析器が読んだ語(全部の改行を消して読んだもの)。
|
|
6
|
+
*
|
|
7
|
+
* Markdown の段落は改行を消して表示されるが、書いた人は改行で区切ったつもりのことがある。
|
|
8
|
+
* 番号だけの見出しの行(「2.1 注文の登録\n登録は」)、宛先や署名を 1 行に 1 つ書いた並び。
|
|
9
|
+
* そこを消して読むと「登録登録」が語の重なりになる。行が続いていると語で言えるときだけ消す。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const inside = (token: Token | undefined, part: Span): boolean => token !== undefined && token.span.start < part.start && token.span.end > part.end;
|
|
13
|
+
|
|
14
|
+
type Around = { readonly across: Token | undefined; readonly before: Token | undefined };
|
|
15
|
+
|
|
16
|
+
/** 各改行の位置で、改行をまたぐ語(あれば)と、改行の直前で終わる語。tokens は並び順(重ならない)。 */
|
|
17
|
+
const aroundEach = (breaks: readonly Span[], tokens: readonly Token[]): Around[] => {
|
|
18
|
+
const spans = tokens.map((token) => token.span);
|
|
19
|
+
return breaks.map((part) => {
|
|
20
|
+
const at = firstEndingAfter(spans, part.start);
|
|
21
|
+
return { across: tokens[at], before: tokens[at - 1] };
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** 解析器が語の途中に置いた改行。語をまたぐなら、行を折り返しただけ。 */
|
|
26
|
+
export const wrapBreaks = (breaks: readonly Span[], tokens: readonly Token[]): Span[] => {
|
|
27
|
+
const around = aroundEach(breaks, tokens);
|
|
28
|
+
return breaks.filter((part, index) => inside(around[index]?.across, part));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** 句を閉じない語。助詞・接続助詞・接続詞で終わる行は、次の行へ続く。 */
|
|
32
|
+
const CONTINUING_POS = new Set(["ADP", "SCONJ", "CCONJ"]);
|
|
33
|
+
/** 読点で終わる行も続く。句点・括弧・名詞で終わる行は、見出しや 1 行 1 項目の終わりと見分けられない。 */
|
|
34
|
+
const COMMAS = new Set(["、", ","]);
|
|
35
|
+
|
|
36
|
+
const continuesPast = (token: Token | undefined, part: Span): boolean =>
|
|
37
|
+
token !== undefined && token.span.end === part.start && (CONTINUING_POS.has(token.pos) || COMMAS.has(token.surface));
|
|
38
|
+
|
|
39
|
+
/** 行が続いている改行: 語をまたぐものと、句を閉じない語の直後にあるもの。 */
|
|
40
|
+
export const continuedBreaks = (breaks: readonly Span[], tokens: readonly Token[]): Span[] => {
|
|
41
|
+
const around = aroundEach(breaks, tokens);
|
|
42
|
+
return breaks.filter((part, index) => inside(around[index]?.across, part) || continuesPast(around[index]?.before, part));
|
|
43
|
+
};
|
package/src/measure.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withoutSpans } from "./soft-break.ts";
|
|
1
2
|
import type { Sentence } from "./plugin.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -9,7 +10,14 @@ import type { Sentence } from "./plugin.ts";
|
|
|
9
10
|
*
|
|
10
11
|
* 日本語は文字数、英語は語数。単位は adapter が宣言する。
|
|
11
12
|
*/
|
|
12
|
-
|
|
13
|
+
/** 語の途中で折り返した改行(wrapBreaks)は空白にせず除く。「関\nする」を「関 する」と読まない。 */
|
|
14
|
+
export const proseText = (sentence: Sentence): string =>
|
|
15
|
+
withoutSpans(
|
|
16
|
+
sentence.text,
|
|
17
|
+
(sentence.wrapBreaks ?? []).map((span) => ({ start: span.start - sentence.span.start, end: span.end - sentence.span.start })),
|
|
18
|
+
)
|
|
19
|
+
.replace(/\s+/gu, " ")
|
|
20
|
+
.trim();
|
|
13
21
|
|
|
14
22
|
export const charLength = (sentence: Sentence): number => sentence.text.replace(/\s+/gu, "").length;
|
|
15
23
|
|
package/src/number-name.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { hyphenGroups, isHyphen } from "./orthography.ts";
|
|
1
2
|
import type { Span, Token } from "./plugin.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 数量ではなく名前として書かれた数(番号・識別子)。鉤括弧で引いた「3.1 リサーチの原則」の節番号、※1 のような注の番号、
|
|
5
|
-
* 「〒
|
|
6
|
+
* 「〒100-8916 東京都」の郵便番号、「紀尾井町1-3 東京ガーデンテラス」の番地は、後ろの空白が番号と題・項目の区切りで、「3回」「3 回」のような空け方の好みではない。
|
|
6
7
|
* 空け方を数える材料にしない。
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* 形で名前と読むのは五つだけ。0 で始まる組を含むハイフンつなぎの番号、※・〒や鉤括弧のすぐ後ろの番号、市・区・町まで下りた地名のすぐ後ろのハイフンつなぎの番号、
|
|
10
|
+
* 文頭の階層つきの節番号(2.1、3.1.2)、続き番号になった行の頭の番号(白書の注)。文頭でも「223 言語」「1ターン」は数量なので、階層の無い数は文頭というだけでは名前と読まない。
|
|
10
11
|
* そのうえで、すぐ後ろ(空白 1 つまで)の語が数につく語(助数詞・数・助詞・助動詞)なら数量として数え続ける(「3-5 日」「26.7 万行」)。
|
|
11
12
|
* 品詞が無ければ判断せず、名前とは読まない。
|
|
12
13
|
*/
|
|
@@ -14,8 +15,8 @@ import type { Span, Token } from "./plugin.ts";
|
|
|
14
15
|
/** 数と結びついて読まれる語。助数詞は NounType=Class、数は NumType=Card で渡る。 */
|
|
15
16
|
const BOUND_TO_NUMBER = new Set(["ADP", "PART", "AUX", "SCONJ", "CCONJ"]);
|
|
16
17
|
|
|
17
|
-
/** 番号が立つ位置の直前:
|
|
18
|
-
const LABEL_LEAD = /[
|
|
18
|
+
/** 番号が立つ位置の直前: 題を包む鉤括弧、注の印、郵便番号の印。 */
|
|
19
|
+
const LABEL_LEAD = /[「『【※〒]$/u;
|
|
19
20
|
|
|
20
21
|
/** 文頭(前に空白と箇条書きの印だけ)。 */
|
|
21
22
|
const LINE_HEAD = /^[\s\-*+・•]*$/u;
|
|
@@ -28,17 +29,18 @@ const SECTION_NUMBER = /^\d+(?:\.\d+)+$/u;
|
|
|
28
29
|
* 数え続ける。三つ以上つないだもの(073-489-5909)は、orthography.ts が境目を作る前に外している。
|
|
29
30
|
*/
|
|
30
31
|
const isHyphenIdentifier = (digits: string): boolean => {
|
|
31
|
-
const parts = digits
|
|
32
|
+
const parts = hyphenGroups(digits);
|
|
32
33
|
return parts.length > 1 && parts.some((part) => part.startsWith("0"));
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
const
|
|
36
|
+
const DIGIT_OR_POINT = /[\d.]/u;
|
|
37
|
+
const isRunChar = (char: string | undefined): boolean => DIGIT_OR_POINT.test(char ?? "") || isHyphen(char);
|
|
36
38
|
|
|
37
|
-
/** at
|
|
39
|
+
/** at を含む、数字・小数点・ハイフン(全角の「-」なども)の並び。at が並びの中に無ければ undefined。 */
|
|
38
40
|
export const digitRunAround = (text: string, at: number): Span | undefined => {
|
|
39
|
-
if (!
|
|
40
|
-
const startAt = (index: number): number => (index > 0 &&
|
|
41
|
-
const endAt = (index: number): number => (index < text.length &&
|
|
41
|
+
if (!isRunChar(text[at])) return undefined;
|
|
42
|
+
const startAt = (index: number): number => (index > 0 && isRunChar(text[index - 1]) ? startAt(index - 1) : index);
|
|
43
|
+
const endAt = (index: number): number => (index < text.length && isRunChar(text[index]) ? endAt(index + 1) : index);
|
|
42
44
|
return { start: startAt(at), end: endAt(at) };
|
|
43
45
|
};
|
|
44
46
|
|
|
@@ -71,6 +73,24 @@ export const sequenceLabelStarts = (text: string): ReadonlySet<number> => {
|
|
|
71
73
|
return new Set(lines.flatMap((line, index) => (continues(index) ? [line.start] : [])));
|
|
72
74
|
};
|
|
73
75
|
|
|
76
|
+
const isGeoUnit = (token: Token): boolean => token.features?.["NameType"] === "GeoUnit";
|
|
77
|
+
const isPlaceWord = (token: Token): boolean => isGeoUnit(token) || token.features?.["NameType"] === "Geo";
|
|
78
|
+
|
|
79
|
+
/** end で終わる、間を空けずに続く地名と地名の単位(東京都千代田区紀尾井町)。近いほうから。 */
|
|
80
|
+
export const placeChainBefore = (tokens: readonly Token[], end: number): Token[] => {
|
|
81
|
+
const last = tokens.find((token) => token.span.end === end && isPlaceWord(token));
|
|
82
|
+
return last === undefined ? [] : [last, ...placeChainBefore(tokens, last.span.start)];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 住所の番地(千代田区紀尾井町1-3、霞が関2-1)。都道府県より下の単位(市・区・町)まで下りた地名のすぐ後ろの、ハイフンつなぎの番号。
|
|
87
|
+
* 地名だけ(北海道2-3 営業日)や都道府県まで(東京都2-3 営業日)の後ろは、地域ごとの範囲のことがあるので読まない。
|
|
88
|
+
* ハイフンの無い数(千代田区23 番)も読まない。topUnits は都道府県の単位(語彙表 prefecture-unit)。
|
|
89
|
+
*/
|
|
90
|
+
const isAddressNumber = (text: string, run: Span, tokens: readonly Token[], base: number, topUnits: ReadonlySet<string>): boolean =>
|
|
91
|
+
[...text.slice(run.start, run.end)].some(isHyphen) &&
|
|
92
|
+
placeChainBefore(tokens, base + run.start).some((token) => isGeoUnit(token) && !topUnits.has(token.surface));
|
|
93
|
+
|
|
74
94
|
/** ハイフンでつないだ識別子か、番号の立つ位置の番号か、文頭の節番号か。 */
|
|
75
95
|
const isNameShaped = (text: string, run: Span): boolean => {
|
|
76
96
|
const [before, digits] = [text.slice(0, run.start), text.slice(run.start, run.end)];
|
|
@@ -78,7 +98,8 @@ const isNameShaped = (text: string, run: Span): boolean => {
|
|
|
78
98
|
};
|
|
79
99
|
|
|
80
100
|
/**
|
|
81
|
-
* text の run が名前として書かれた数か。tokens は文書全体の座標で、base は text の先頭の位置。sequence は sequenceLabelStarts
|
|
101
|
+
* text の run が名前として書かれた数か。tokens は文書全体の座標で、base は text の先頭の位置。sequence は sequenceLabelStarts の結果、
|
|
102
|
+
* topUnits は都道府県の単位。
|
|
82
103
|
* 後ろの語が読めない(品詞が無い、語の切れ目が合わない)ときは、数量として数え続ける。
|
|
83
104
|
*/
|
|
84
105
|
export const isNumberName = (
|
|
@@ -87,9 +108,10 @@ export const isNumberName = (
|
|
|
87
108
|
tokens: readonly Token[] | undefined,
|
|
88
109
|
base: number,
|
|
89
110
|
sequence: ReadonlySet<number> = new Set(),
|
|
111
|
+
topUnits: ReadonlySet<string> = new Set(),
|
|
90
112
|
): boolean => {
|
|
91
113
|
const inSequence = sequence.has(base + run.start);
|
|
92
|
-
if (tokens === undefined || !(inSequence || isNameShaped(text, run))) return false;
|
|
114
|
+
if (tokens === undefined || !(inSequence || isNameShaped(text, run) || isAddressNumber(text, run, tokens, base, topUnits))) return false;
|
|
93
115
|
const next = wordAfter(tokens, text, run, base);
|
|
94
116
|
if (next === undefined) return false;
|
|
95
117
|
return !isBoundToNumber(next) || (inSequence && CONJUNCTIONS.has(next.pos));
|
package/src/orthography.ts
CHANGED
|
@@ -125,22 +125,28 @@ const isOrdinalRun = (chars: readonly string[], index: number): boolean => {
|
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* 「073-489-5909」「2026-06-02」「Ⅰ-4-1-3」のように - でつないだ 3
|
|
128
|
+
* 「073-489-5909」「2026-06-02」「Ⅰ-4-1-3」「5-1-1」のように - でつないだ 3 組以上の数字は、電話番号・日付・図や項目の番号で
|
|
129
129
|
* 数量ではない。後ろの空白は欄の区切りで、「3回」の空け方とは別のもの。両側とも数えない。
|
|
130
130
|
* 2 組(「1-3ヶ月」「20-30分」)は幅のある数量なので数える。
|
|
131
131
|
*/
|
|
132
132
|
const MIN_CODE_GROUPS = 3;
|
|
133
|
-
|
|
133
|
+
|
|
134
|
+
// 通達の「5-1-1」は全角の「-」でつなぐ。ハイフン「‐」やマイナス「−」も同じに読む。長音「ー」は仮名の一部なので入れない。
|
|
135
|
+
const HYPHEN = /[-‐‑−-]/u;
|
|
136
|
+
|
|
137
|
+
/** 数字をつなぐハイフンか。半角の - のほか、全角・和文のハイフンも。 */
|
|
138
|
+
export const isHyphen = (char: string | undefined): boolean => char !== undefined && HYPHEN.test(char);
|
|
139
|
+
|
|
140
|
+
/** ハイフンで区切った組(空の組は除く)。「5-1-1」は ["5", "1", "1"]。 */
|
|
141
|
+
export const hyphenGroups = (text: string): string[] => text.split(HYPHEN).filter((group) => group !== "");
|
|
142
|
+
|
|
143
|
+
const isCodeChar = (char: string | undefined): boolean => DIGIT.test(char ?? "") || isHyphen(char);
|
|
134
144
|
|
|
135
145
|
const isCode = (chars: readonly string[], digit: number): boolean => {
|
|
136
146
|
let [first, last] = [digit, digit];
|
|
137
|
-
while (first > 0 &&
|
|
138
|
-
while (last < chars.length - 1 &&
|
|
139
|
-
|
|
140
|
-
.slice(first, last + 1)
|
|
141
|
-
.join("")
|
|
142
|
-
.split("-");
|
|
143
|
-
return groups.filter((group) => group !== "").length >= MIN_CODE_GROUPS;
|
|
147
|
+
while (first > 0 && isCodeChar(chars[first - 1])) first -= 1;
|
|
148
|
+
while (last < chars.length - 1 && isCodeChar(chars[last + 1])) last += 1;
|
|
149
|
+
return hyphenGroups(chars.slice(first, last + 1).join("")).length >= MIN_CODE_GROUPS;
|
|
144
150
|
};
|
|
145
151
|
|
|
146
152
|
/** 日本語との境目にある数字が、数えない書き方(「第3条」の番地、「073-489-5909」の符号)か。 */
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Markdown parser drops a leading BOM and reads a lone "\r" as a line break; the rest of chaff counts "\n" only.
|
|
3
|
+
* Reading the file this way keeps every offset, line and column on the text the writer sees.
|
|
4
|
+
*/
|
|
5
|
+
export const plainSource = (raw: string): string => raw.replace(/^\uFEFF/u, "").replace(/\r\n?/gu, "\n");
|
package/src/plugin.ts
CHANGED
|
@@ -8,7 +8,16 @@ export type Span = { readonly start: number; readonly end: number };
|
|
|
8
8
|
* token の span は文の span と同じ座標系(segment に渡した文字列の先頭が 0)。
|
|
9
9
|
* 2 つの基準を混ぜると、ずれたときに どちらが悪いか分からなくなる。
|
|
10
10
|
*/
|
|
11
|
-
export type Sentence = {
|
|
11
|
+
export type Sentence = {
|
|
12
|
+
readonly span: Span;
|
|
13
|
+
readonly text: string;
|
|
14
|
+
readonly tokens?: readonly Token[];
|
|
15
|
+
/**
|
|
16
|
+
* 行の折り返しで語の途中に入った改行(前後の行頭・行末の空白ごと)。span と同じ座標系。chaff が入れる。
|
|
17
|
+
* tokens は全角どうしに挟まれた段落内の改行を除いた文字列から作るので、改行をまたぐ語の span は改行ごと覆い、surface より長い。
|
|
18
|
+
*/
|
|
19
|
+
readonly wrapBreaks?: readonly Span[];
|
|
20
|
+
};
|
|
12
21
|
|
|
13
22
|
/**
|
|
14
23
|
* 品詞は Universal Dependencies の UPOS に統一する。アダプタ独自の体系を露出させない。
|
|
@@ -133,7 +142,10 @@ export type StructurePatterns = {
|
|
|
133
142
|
readonly numbered: (line: string, context: NumberingContext) => NumberedLine | undefined;
|
|
134
143
|
/** 定義。attrs.term に定義された語。 */
|
|
135
144
|
readonly definitions: (text: string) => readonly Mention[];
|
|
136
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* 参照。attrs.target に正規化した番地("12.1")、attrs.label に書かれたまま。他の文書を指すなら attrs.document にその名。
|
|
147
|
+
* 書き方だけでは他の文書か決まらない角括弧の語は attrs.citedTag に入れ、文書がその語を一覧に載せていれば core が他の文書とみなす。
|
|
148
|
+
*/
|
|
137
149
|
readonly references: (text: string) => readonly Mention[];
|
|
138
150
|
/** 義務・禁止・許可。attrs.marker に語、attrs.type に must / must-not / may。 */
|
|
139
151
|
readonly obligations: (text: string) => readonly Mention[];
|
|
@@ -156,6 +168,11 @@ export type StructurePatterns = {
|
|
|
156
168
|
* 数字と点だけの通し番号は core が言語を問わず読むので、それを数量と見分けられるのは言語パッケージだけ。
|
|
157
169
|
*/
|
|
158
170
|
readonly countedAfter?: (number: string, rest: string) => boolean;
|
|
171
|
+
/**
|
|
172
|
+
* 本文の行頭の「3.11.0 を公開しました。」の 3.11.0 は、文の中の数で通し番号ではない。番号と後ろの文字列を渡し、文の続き
|
|
173
|
+
* (日本語なら助詞で始まる)なら true。小文字で始まる続きは core が言語を問わず読む。
|
|
174
|
+
*/
|
|
175
|
+
readonly continuesSentence?: (number: string, rest: string) => boolean;
|
|
159
176
|
/** 数の書き方(「二十二」「3」)を数にする。相対の参照の「前二項」「前条第二項」が使う。 */
|
|
160
177
|
readonly number?: (text: string) => number | undefined;
|
|
161
178
|
};
|
|
@@ -291,6 +308,8 @@ export type ProseDocument = {
|
|
|
291
308
|
readonly paragraphs: readonly Paragraph[];
|
|
292
309
|
/** 箇条書き 1 つ。項目の数と長さのばらつきを見る rule が使う。 */
|
|
293
310
|
readonly lists: readonly BulletList[];
|
|
311
|
+
/** リンク(`[text](url)`、`<https://…>`、`[text][ref]`)の範囲。行き先が相対パスでもページ内でも、読み手が辿れる出典。 */
|
|
312
|
+
readonly links: readonly Span[];
|
|
294
313
|
/** アダプタが持つ語彙表と、チームが chaff.yaml に足した語彙表。detector は出所を知らない。 */
|
|
295
314
|
readonly lexicons: Readonly<Record<string, Lexicon>>;
|
|
296
315
|
/** この種類の文書に無いと困る見出し。チームが chaff.yaml で決める。 */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Span } from "./plugin.ts";
|
|
2
|
+
|
|
3
|
+
/** 鉤括弧の開きと、それを閉じる字。 */
|
|
4
|
+
const CLOSER: Readonly<Record<string, string>> = { "「": "」", "『": "』" };
|
|
5
|
+
|
|
6
|
+
type Open = { readonly closer: string; readonly at: number };
|
|
7
|
+
type Scan = { readonly open: readonly Open[]; readonly spans: readonly Span[] };
|
|
8
|
+
|
|
9
|
+
const step = (scan: Scan, char: string, at: number): Scan => {
|
|
10
|
+
const closer = CLOSER[char];
|
|
11
|
+
if (closer !== undefined) return { open: [...scan.open, { closer, at }], spans: scan.spans };
|
|
12
|
+
const last = scan.open.at(-1);
|
|
13
|
+
if (last === undefined || last.closer !== char) return scan;
|
|
14
|
+
return { open: scan.open.slice(0, -1), spans: [...scan.spans, { start: last.at + 1, end: at }] };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 鉤括弧(「」『』)の中身の位置。括弧そのものは含まない。入れ子はそれぞれを返す。
|
|
19
|
+
* 閉じない括弧と、開きと組にならない閉じ括弧は中身を作らない。位置は text の先頭を 0 とする。
|
|
20
|
+
* 括弧は BMP の字なので、UTF-16 の 1 単位ずつ見れば位置がそのまま使える。
|
|
21
|
+
*/
|
|
22
|
+
export const quotedSpans = (text: string): Span[] => [...text.split("").reduce<Scan>((scan, char, at) => step(scan, char, at), { open: [], spans: [] }).spans];
|
|
23
|
+
|
|
24
|
+
/** inner が、spans のどれかにまるごと入っているか。 */
|
|
25
|
+
export const isWithinAny = (spans: readonly Span[], inner: Span): boolean => spans.some((span) => inner.start >= span.start && inner.end <= span.end);
|
package/src/render/eval.ts
CHANGED
|
@@ -1,62 +1,125 @@
|
|
|
1
|
-
import { MIN_CORPUS, TARGET_HIT_RATE, type RuleReport } from "../eval.ts";
|
|
1
|
+
import { MIN_CORPUS, TARGET_HIT_RATE, type Point, type RuleReport } from "../eval.ts";
|
|
2
|
+
import type { Texts, UiLanguage } from "../ui.ts";
|
|
2
3
|
|
|
3
4
|
const pct = (part: number, whole: number): string => `${((part / Math.max(1, whole)) * 100).toFixed(1)}%`;
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const TARGET = TARGET_HIT_RATE * 100;
|
|
7
|
+
|
|
8
|
+
/** In the English hint, a rule's line sits one YAML level under `rules:`. */
|
|
9
|
+
const EN_CONFIG_LEAD = " In chaff.yaml: ";
|
|
10
|
+
const YAML_INDENT = 2;
|
|
11
|
+
|
|
12
|
+
const TEXT: Texts<{
|
|
13
|
+
readonly current: string;
|
|
14
|
+
readonly recommended: string;
|
|
15
|
+
readonly both: string;
|
|
16
|
+
readonly point: (point: Point, share: string) => string;
|
|
17
|
+
readonly fits: string;
|
|
18
|
+
readonly nothingFits: string;
|
|
19
|
+
readonly over: (share: string) => string;
|
|
20
|
+
readonly recommend: (limit: number) => string;
|
|
21
|
+
readonly toConfig: (rule: string, limit: number) => string;
|
|
22
|
+
readonly measured: (paths: number, rules: number) => string;
|
|
23
|
+
readonly premise: readonly string[];
|
|
24
|
+
readonly smallCorpus: (total: number) => readonly string[];
|
|
25
|
+
readonly untouched: string;
|
|
26
|
+
}> = {
|
|
27
|
+
ja: {
|
|
28
|
+
current: " ← 現在",
|
|
29
|
+
recommended: " ← 推奨",
|
|
30
|
+
both: " ← 現在 / 推奨",
|
|
31
|
+
point: (point, share) =>
|
|
32
|
+
`${String(point.documents).padStart(3)} 文書 (${share.padStart(6)}) 指摘 ${String(point.findings).padStart(3)} 件 1万字あたり ${point.per10k.toFixed(1)}`,
|
|
33
|
+
fits: ` いまの閾値で目標(${TARGET}% 未満)を満たしています。`,
|
|
34
|
+
nothingFits: ` どの閾値でも目標を満たしません。この corpus に対して rule 自体が合っていない可能性があります。`,
|
|
35
|
+
over: (share) => ` いまの閾値では ${share} の文書が該当し、目標(${TARGET}% 未満)を超えています。`,
|
|
36
|
+
recommend: (limit) => ` 推奨: ${limit}`,
|
|
37
|
+
toConfig: (rule, limit) => ` chaff.yaml に書くなら: rules:\n ${rule}: ${limit}`,
|
|
38
|
+
measured: (paths, rules) => ` ${paths} ファイルを corpus として ${rules} 本の rule を測りました。`,
|
|
39
|
+
premise: [
|
|
40
|
+
" ここにある文書は「人間が書いて公開した良い文書」として扱っています。",
|
|
41
|
+
` そこで多く発火する rule は、閾値が現実に合っていない疑いがあります(目標: ${TARGET}% 未満)。`,
|
|
42
|
+
],
|
|
43
|
+
smallCorpus: (total) => [
|
|
44
|
+
` ※ corpus が ${total} 文書しかありません。${TARGET}% を表すには ${MIN_CORPUS} 文書以上が要ります。`,
|
|
45
|
+
" いまは割合が「0 か全部」に振れるので、右端の密度(1万字あたりの指摘数)のほうを見てください。",
|
|
46
|
+
],
|
|
47
|
+
untouched: ` 閾値は自動で書き換えていません。corpus が変われば答えも変わるためです。`,
|
|
48
|
+
},
|
|
49
|
+
en: {
|
|
50
|
+
current: " ← current",
|
|
51
|
+
recommended: " ← recommended",
|
|
52
|
+
both: " ← current / recommended",
|
|
53
|
+
point: (point, share) =>
|
|
54
|
+
`${String(point.documents).padStart(3)} ${point.documents === 1 ? "doc " : "docs"} (${share.padStart(6)}) ${String(point.findings).padStart(3)} ${point.findings === 1 ? "finding " : "findings"} ${point.per10k.toFixed(1)} per 10,000 characters`,
|
|
55
|
+
fits: ` The current limit meets the target (under ${TARGET}%).`,
|
|
56
|
+
nothingFits: " No limit meets the target. The rule itself may not suit this corpus.",
|
|
57
|
+
over: (share) => ` At the current limit ${share} of the documents are hit, over the target (under ${TARGET}%).`,
|
|
58
|
+
recommend: (limit) => ` Recommended: ${limit}`,
|
|
59
|
+
toConfig: (rule, limit) => `${EN_CONFIG_LEAD}rules:\n${" ".repeat(EN_CONFIG_LEAD.length + YAML_INDENT)}${rule}: ${limit}`,
|
|
60
|
+
measured: (paths, rules) => ` Measured ${rules} rule${rules === 1 ? "" : "s"} on ${paths} file${paths === 1 ? "" : "s"} as a corpus.`,
|
|
61
|
+
premise: [
|
|
62
|
+
" The documents here are taken as good documents that people wrote and published.",
|
|
63
|
+
` A rule that fires often on them may have a limit that does not fit real writing (target: under ${TARGET}%).`,
|
|
64
|
+
],
|
|
65
|
+
smallCorpus: (total) => [
|
|
66
|
+
` Note: the corpus has only ${total} document${total === 1 ? "" : "s"}. Showing ${TARGET}% takes at least ${MIN_CORPUS}.`,
|
|
67
|
+
" For now each share is either 0 or all, so read the density on the right (findings per 10,000 characters).",
|
|
68
|
+
],
|
|
69
|
+
untouched: " No limit was changed. A different corpus gives a different answer.",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type EvalText = (typeof TEXT)[UiLanguage];
|
|
74
|
+
|
|
75
|
+
const mark = (report: RuleReport, limit: number, text: EvalText): string => {
|
|
76
|
+
if (limit === report.current && limit === report.recommended) return text.both;
|
|
77
|
+
if (limit === report.current) return text.current;
|
|
78
|
+
if (limit === report.recommended) return text.recommended;
|
|
9
79
|
return "";
|
|
10
80
|
};
|
|
11
81
|
|
|
12
|
-
const sweepLines = (report: RuleReport): string[] =>
|
|
13
|
-
report.sweep.map(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const density = `1万字あたり ${point.per10k.toFixed(1)}`;
|
|
17
|
-
return ` ${limit} ${docs} (${pct(point.documents, report.total).padStart(6)}) 指摘 ${String(point.findings).padStart(3)} 件 ${density}${mark(report, point.limit)}`;
|
|
18
|
-
});
|
|
82
|
+
const sweepLines = (report: RuleReport, text: EvalText): string[] =>
|
|
83
|
+
report.sweep.map(
|
|
84
|
+
(point) => ` ${String(point.limit).padStart(6)} ${text.point(point, pct(point.documents, report.total))}${mark(report, point.limit, text)}`,
|
|
85
|
+
);
|
|
19
86
|
|
|
20
|
-
const verdict = (report: RuleReport): string[] => {
|
|
87
|
+
const verdict = (report: RuleReport, text: EvalText): string[] => {
|
|
21
88
|
const now = report.sweep.find((point) => point.limit === report.current);
|
|
22
89
|
const rate = now === undefined ? 0 : now.documents / Math.max(1, report.total);
|
|
23
|
-
if (rate <= TARGET_HIT_RATE) return [
|
|
24
|
-
if (report.recommended === undefined)
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
return [
|
|
28
|
-
` いまの閾値では ${pct(now?.documents ?? 0, report.total)} の文書が該当し、目標(${TARGET_HIT_RATE * 100}% 未満)を超えています。`,
|
|
29
|
-
` 推奨: ${report.recommended}`,
|
|
30
|
-
"",
|
|
31
|
-
` chaff.yaml に書くなら: rules:\n ${report.rule}: ${report.recommended}`,
|
|
32
|
-
];
|
|
90
|
+
if (rate <= TARGET_HIT_RATE) return [text.fits];
|
|
91
|
+
if (report.recommended === undefined) return [text.nothingFits];
|
|
92
|
+
return [text.over(pct(now?.documents ?? 0, report.total)), text.recommend(report.recommended), "", text.toConfig(report.rule, report.recommended)];
|
|
33
93
|
};
|
|
34
94
|
|
|
35
|
-
const block = (report: RuleReport): string[] => [
|
|
95
|
+
const block = (report: RuleReport, text: EvalText): string[] => [
|
|
96
|
+
"",
|
|
97
|
+
` ${report.name} (${report.rule})`,
|
|
98
|
+
"",
|
|
99
|
+
...sweepLines(report, text),
|
|
100
|
+
"",
|
|
101
|
+
...verdict(report, text),
|
|
102
|
+
"",
|
|
103
|
+
];
|
|
36
104
|
|
|
37
105
|
/**
|
|
38
106
|
* 閾値は自動で書き換えない。提示して選ばせる。
|
|
39
107
|
* 較正は corpus に対する答えであって、正解ではないため。
|
|
40
108
|
*/
|
|
41
|
-
export const renderEval = (reports: readonly RuleReport[], total: number, paths: number): string =>
|
|
42
|
-
[
|
|
109
|
+
export const renderEval = (reports: readonly RuleReport[], total: number, paths: number, ui: UiLanguage): string => {
|
|
110
|
+
const text = TEXT[ui];
|
|
111
|
+
return [
|
|
43
112
|
"",
|
|
44
|
-
|
|
113
|
+
text.measured(paths, reports.length),
|
|
45
114
|
"",
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
...(
|
|
49
|
-
? [
|
|
50
|
-
"",
|
|
51
|
-
` ※ corpus が ${total} 文書しかありません。${TARGET_HIT_RATE * 100}% を表すには ${MIN_CORPUS} 文書以上が要ります。`,
|
|
52
|
-
" いまは割合が「0 か全部」に振れるので、右端の密度(1万字あたりの指摘数)のほうを見てください。",
|
|
53
|
-
]
|
|
54
|
-
: []),
|
|
55
|
-
...reports.flatMap(block),
|
|
115
|
+
...text.premise,
|
|
116
|
+
...(total < MIN_CORPUS ? ["", ...text.smallCorpus(total)] : []),
|
|
117
|
+
...reports.flatMap((report) => block(report, text)),
|
|
56
118
|
" " + "─".repeat(58),
|
|
57
119
|
"",
|
|
58
|
-
|
|
120
|
+
text.untouched,
|
|
59
121
|
"",
|
|
60
122
|
]
|
|
61
123
|
.join("\n")
|
|
62
124
|
.replace(/\{total\}/gu, String(total));
|
|
125
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { applyByPath } from "./config/by-path.ts";
|
|
2
|
+
import type { Config } from "./config/load.ts";
|
|
3
|
+
import type { GenreSource } from "./cli-text.ts";
|
|
4
|
+
import { frontMatterGenre, GENRES, guessGenre } from "./genre.ts";
|
|
5
|
+
import { unknownFrontMatterGenre } from "./genre-check.ts";
|
|
6
|
+
|
|
7
|
+
/** unread: a genre the front matter wrote that chaff does not know, when the front matter was looked at. */
|
|
8
|
+
type ResolvedGenre = { readonly genre: string; readonly from: GenreSource; readonly unread?: string | undefined };
|
|
9
|
+
|
|
10
|
+
export const resolveGenre = (path: string, source: string, config: Config, cliGenre?: string): ResolvedGenre => {
|
|
11
|
+
// コマンドで指定したものが最優先。その実行だけの指定だから、設定より強い。
|
|
12
|
+
if (cliGenre !== undefined) return { genre: cliGenre, from: "--genre" };
|
|
13
|
+
// パスごとの上書きが次。「全体はこう、ここだけは違う」を書けるようにする。
|
|
14
|
+
const override = applyByPath(config.byPath, config.baseDir, path);
|
|
15
|
+
if (override.genre !== undefined) return { genre: override.genre, from: "by_path" };
|
|
16
|
+
if (config.genre !== undefined) return { genre: config.genre, from: "config" };
|
|
17
|
+
const guess = guessGenre(path, source, frontMatterGenre(source));
|
|
18
|
+
const unread = unknownFrontMatterGenre(source, GENRES);
|
|
19
|
+
return guess === undefined ? { genre: "blog/tech", from: "default", unread } : { genre: guess.genre, from: guess.from, unread };
|
|
20
|
+
};
|