chaffjs 0.10.0 → 0.12.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/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +2 -0
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -1
- package/dist/cli.js.map +1 -1
- package/dist/date-stamp.d.ts +14 -0
- package/dist/date-stamp.d.ts.map +1 -0
- package/dist/date-stamp.js +40 -0
- package/dist/date-stamp.js.map +1 -0
- package/dist/declared-lists.d.ts +4 -0
- package/dist/declared-lists.d.ts.map +1 -0
- package/dist/declared-lists.js +3 -0
- package/dist/declared-lists.js.map +1 -0
- package/dist/detectors/acronym-context.d.ts +13 -0
- package/dist/detectors/acronym-context.d.ts.map +1 -0
- package/dist/detectors/acronym-context.js +42 -0
- package/dist/detectors/acronym-context.js.map +1 -0
- package/dist/detectors/acronym-expansion.d.ts +9 -0
- package/dist/detectors/acronym-expansion.d.ts.map +1 -0
- package/dist/detectors/acronym-expansion.js +67 -0
- package/dist/detectors/acronym-expansion.js.map +1 -0
- package/dist/detectors/char-shape.d.ts.map +1 -1
- package/dist/detectors/char-shape.js +34 -2
- package/dist/detectors/char-shape.js.map +1 -1
- package/dist/detectors/doubled-word.d.ts +27 -0
- package/dist/detectors/doubled-word.d.ts.map +1 -0
- package/dist/detectors/doubled-word.js +94 -0
- package/dist/detectors/doubled-word.js.map +1 -0
- package/dist/detectors/en-shape.d.ts.map +1 -1
- package/dist/detectors/en-shape.js +110 -9
- package/dist/detectors/en-shape.js.map +1 -1
- package/dist/detectors/gram-place.d.ts +14 -0
- package/dist/detectors/gram-place.d.ts.map +1 -0
- package/dist/detectors/gram-place.js +39 -0
- package/dist/detectors/gram-place.js.map +1 -0
- package/dist/detectors/heading-echo.d.ts.map +1 -1
- package/dist/detectors/heading-echo.js +22 -12
- package/dist/detectors/heading-echo.js.map +1 -1
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +2 -0
- package/dist/detectors/index.js.map +1 -1
- package/dist/detectors/lead-in.d.ts +8 -0
- package/dist/detectors/lead-in.d.ts.map +1 -0
- package/dist/detectors/lead-in.js +13 -0
- package/dist/detectors/lead-in.js.map +1 -0
- package/dist/detectors/lexicon-match.d.ts +15 -0
- package/dist/detectors/lexicon-match.d.ts.map +1 -0
- package/dist/detectors/lexicon-match.js +48 -0
- package/dist/detectors/lexicon-match.js.map +1 -0
- package/dist/detectors/lexicon.d.ts +5 -0
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +32 -22
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/phrase-match.d.ts.map +1 -1
- package/dist/detectors/phrase-match.js +7 -6
- package/dist/detectors/phrase-match.js.map +1 -1
- package/dist/detectors/repeated-head.d.ts.map +1 -1
- package/dist/detectors/repeated-head.js +15 -5
- package/dist/detectors/repeated-head.js.map +1 -1
- package/dist/detectors/sentence-ending.d.ts.map +1 -1
- package/dist/detectors/sentence-ending.js +7 -3
- package/dist/detectors/sentence-ending.js.map +1 -1
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +97 -59
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/structure.d.ts.map +1 -1
- package/dist/detectors/structure.js +14 -1
- package/dist/detectors/structure.js.map +1 -1
- package/dist/detectors/superlative-scope.d.ts +16 -0
- package/dist/detectors/superlative-scope.d.ts.map +1 -0
- package/dist/detectors/superlative-scope.js +40 -0
- package/dist/detectors/superlative-scope.js.map +1 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +38 -19
- package/dist/document.js.map +1 -1
- package/dist/eval.d.ts.map +1 -1
- package/dist/eval.js +3 -2
- package/dist/eval.js.map +1 -1
- package/dist/heading-title.d.ts +6 -0
- package/dist/heading-title.d.ts.map +1 -0
- package/dist/heading-title.js +8 -0
- package/dist/heading-title.js.map +1 -0
- package/dist/in-page-nav.d.ts +19 -0
- package/dist/in-page-nav.d.ts.map +1 -0
- package/dist/in-page-nav.js +44 -0
- package/dist/in-page-nav.js.map +1 -0
- package/dist/lexicon-tokens.d.ts +4 -0
- package/dist/lexicon-tokens.d.ts.map +1 -0
- package/dist/lexicon-tokens.js +23 -0
- package/dist/lexicon-tokens.js.map +1 -0
- package/dist/orthography.d.ts +1 -0
- package/dist/orthography.d.ts.map +1 -1
- package/dist/orthography.js +4 -3
- package/dist/orthography.js.map +1 -1
- package/dist/page-furniture.d.ts +10 -0
- package/dist/page-furniture.d.ts.map +1 -0
- package/dist/page-furniture.js +35 -0
- package/dist/page-furniture.js.map +1 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/render/compact.d.ts.map +1 -1
- package/dist/render/compact.js +11 -2
- package/dist/render/compact.js.map +1 -1
- package/dist/rule-load.d.ts.map +1 -1
- package/dist/rule-load.js +2 -0
- package/dist/rule-load.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +10 -7
- package/dist/run.js.map +1 -1
- package/dist/sentence-shape.d.ts +6 -0
- package/dist/sentence-shape.d.ts.map +1 -1
- package/dist/sentence-shape.js +15 -0
- package/dist/sentence-shape.js.map +1 -1
- package/dist/structure/bare-numbers.d.ts +14 -0
- package/dist/structure/bare-numbers.d.ts.map +1 -0
- package/dist/structure/bare-numbers.js +53 -0
- package/dist/structure/bare-numbers.js.map +1 -0
- package/dist/structure/build.d.ts +2 -0
- package/dist/structure/build.d.ts.map +1 -1
- package/dist/structure/build.js +6 -3
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +11 -2
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/of.d.ts.map +1 -1
- package/dist/structure/of.js +4 -2
- package/dist/structure/of.js.map +1 -1
- package/dist/structure/total.d.ts +1 -0
- package/dist/structure/total.d.ts.map +1 -1
- package/dist/structure/total.js +37 -13
- package/dist/structure/total.js.map +1 -1
- package/dist/structure/universal.d.ts +2 -2
- package/dist/structure/universal.d.ts.map +1 -1
- package/dist/structure/universal.js +13 -4
- package/dist/structure/universal.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +17 -2
- package/dist/version.js.map +1 -1
- package/package.json +3 -3
- package/rules/ai-tell.yaml +1 -0
- package/rules/closing-cliche.yaml +1 -0
- package/rules/concrete-evidence-density.yaml +2 -0
- package/rules/cushion-phrase-density.yaml +1 -0
- package/rules/double-keigo.yaml +1 -0
- package/rules/doubled-word.yaml +27 -0
- package/rules/empty-intensifier.yaml +1 -0
- package/rules/excessive-hedging.yaml +1 -0
- package/rules/hiragana-fukushi.yaml +1 -0
- package/rules/latin-spacing.yaml +2 -2
- package/rules/max-kanji-continuous.yaml +2 -0
- package/rules/ngram-repetition.yaml +2 -0
- package/rules/padded-intro.yaml +1 -0
- package/rules/repeated-conjunction.yaml +1 -0
- package/rules/sasete-itadaku.yaml +1 -0
- package/rules/undefined-acronym.yaml +1 -0
- package/rules/unqualified-superlative.yaml +2 -0
- package/src/cli-text.ts +2 -0
- package/src/cli.ts +5 -1
- package/src/date-stamp.ts +65 -0
- package/src/declared-lists.ts +5 -0
- package/src/detectors/acronym-context.ts +53 -0
- package/src/detectors/acronym-expansion.ts +81 -0
- package/src/detectors/char-shape.ts +38 -3
- package/src/detectors/doubled-word.ts +128 -0
- package/src/detectors/en-shape.ts +138 -7
- package/src/detectors/gram-place.ts +41 -0
- package/src/detectors/heading-echo.ts +13 -3
- package/src/detectors/index.ts +2 -0
- package/src/detectors/lead-in.ts +14 -0
- package/src/detectors/lexicon-match.ts +54 -0
- package/src/detectors/lexicon.ts +44 -25
- package/src/detectors/phrase-match.ts +8 -7
- package/src/detectors/repeated-head.ts +21 -7
- package/src/detectors/sentence-ending.ts +7 -3
- package/src/detectors/signals.ts +122 -60
- package/src/detectors/structure.ts +21 -2
- package/src/detectors/superlative-scope.ts +66 -0
- package/src/document.ts +43 -19
- package/src/eval.ts +2 -1
- package/src/heading-title.ts +8 -0
- package/src/in-page-nav.ts +67 -0
- package/src/lexicon-tokens.ts +28 -0
- package/src/orthography.ts +4 -3
- package/src/page-furniture.ts +45 -0
- package/src/plugin.ts +13 -1
- package/src/render/compact.ts +12 -2
- package/src/rule-load.ts +2 -0
- package/src/run.ts +11 -7
- package/src/sentence-shape.ts +16 -0
- package/src/structure/bare-numbers.ts +78 -0
- package/src/structure/build.ts +9 -3
- package/src/structure/issues.ts +16 -2
- package/src/structure/of.ts +4 -2
- package/src/structure/total.ts +45 -19
- package/src/structure/universal.ts +14 -3
- package/src/version.ts +21 -3
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Lexicon, Token } from "../plugin.ts";
|
|
2
|
+
import type { TokenRange } from "./lexicon-match.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 最上級が範囲を持っているか。「日本で最も」「国内最大」"the best in the world" は、何の中で最もなのかを言っている。
|
|
6
|
+
* 範囲を示す語は言語で違い、置く側も違う(日本語は最上級の前、英語は後ろ)。どちらの語も言語パッケージの語彙表が持つ。
|
|
7
|
+
*/
|
|
8
|
+
export type ScopeMarkers = {
|
|
9
|
+
/** 最上級の直前に置く語(で)。その前に固有名詞か地名が要る。 */
|
|
10
|
+
readonly before: Lexicon;
|
|
11
|
+
/** 最上級の名詞句の直後に置く語(in・of)。その後ろに名詞句が要る。 */
|
|
12
|
+
readonly after: Lexicon;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** 語彙表の position で分ける。position の無い語は、どちら側の語か分からないので使わない。 */
|
|
16
|
+
export const scopeMarkersOf = (lexicon: Lexicon): ScopeMarkers => ({
|
|
17
|
+
before: lexicon.filter((entry) => entry.position === "before"),
|
|
18
|
+
after: lexicon.filter((entry) => entry.position === "after"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const NOMINAL = new Set(["NOUN", "PROPN"]);
|
|
22
|
+
|
|
23
|
+
/** 名詞句の始まり。the world・Chicago・our products・the three・all。 */
|
|
24
|
+
const PHRASE_START = new Set(["DET", "NOUN", "PROPN", "PRON", "NUM", "ADJ"]);
|
|
25
|
+
|
|
26
|
+
/** 名詞の前に来る修飾(most skilled・widely used・stuffed)。名詞が出た後は名詞の連なりだけが続く。 */
|
|
27
|
+
const PREMODIFIER = new Set(["ADJ", "ADV", "VERB", "NOUN", "PROPN", "NUM"]);
|
|
28
|
+
const NOUN_RUN = new Set(["NOUN", "PROPN", "NUM"]);
|
|
29
|
+
|
|
30
|
+
/** 範囲の前に置ける語。修飾だけで終わった句(the best is in …)は範囲ではなく述語。 */
|
|
31
|
+
const PHRASE_END = new Set(["NOUN", "PROPN", "NUM", "ADJ"]);
|
|
32
|
+
|
|
33
|
+
const isMarker = (token: Token | undefined, markers: Lexicon): boolean =>
|
|
34
|
+
token !== undefined && markers.some((entry) => entry.pattern.toLowerCase() === token.surface.toLowerCase());
|
|
35
|
+
|
|
36
|
+
const isNamed = (token: Token | undefined): boolean => token !== undefined && (token.pos === "PROPN" || token.features?.["NameType"] !== undefined);
|
|
37
|
+
|
|
38
|
+
/** 空白も助詞も挟まずに名詞が付いた最上級(国内最大・業界最速・世界唯一)。前の名詞が範囲。 */
|
|
39
|
+
const compound = (tokens: readonly Token[], range: TokenRange): boolean => {
|
|
40
|
+
const before = tokens[range.start - 1];
|
|
41
|
+
const first = tokens[range.start];
|
|
42
|
+
return before !== undefined && first !== undefined && NOMINAL.has(before.pos) && before.span.end === first.span.start;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** 名前 + 範囲の語 + 最上級(日本で最も・トヨタで最も・東京都で最大)。「費用で最大の効果」の「費用」は名前ではないので範囲にしない。 */
|
|
46
|
+
const namedBefore = (tokens: readonly Token[], range: TokenRange, markers: Lexicon): boolean =>
|
|
47
|
+
isMarker(tokens[range.start - 1], markers) && isNamed(tokens[range.start - 2]);
|
|
48
|
+
|
|
49
|
+
/** 最上級に続く名詞句の終わり。名詞が出るまでは修飾を、出た後は名詞の連なりを読む。 */
|
|
50
|
+
const phraseEnd = (tokens: readonly Token[], at: number, nounSeen = false): number => {
|
|
51
|
+
const token = tokens[at];
|
|
52
|
+
if (token === undefined || !(nounSeen ? NOUN_RUN : PREMODIFIER).has(token.pos)) return at;
|
|
53
|
+
return phraseEnd(tokens, at + 1, nounSeen || NOMINAL.has(token.pos));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** 最上級 + 名詞句 + 範囲の語 + 名詞句(the best pizza in Chicago・the most famous of the sculptures)。 */
|
|
57
|
+
const phraseAfter = (tokens: readonly Token[], range: TokenRange, markers: Lexicon): boolean => {
|
|
58
|
+
const at = phraseEnd(tokens, range.end);
|
|
59
|
+
const last = tokens[at - 1];
|
|
60
|
+
const endsPhrase = at === range.end || (last !== undefined && PHRASE_END.has(last.pos));
|
|
61
|
+
const opens = tokens[at + 1];
|
|
62
|
+
return endsPhrase && isMarker(tokens[at], markers) && opens !== undefined && PHRASE_START.has(opens.pos);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const scoped = (tokens: readonly Token[], range: TokenRange, markers: ScopeMarkers): boolean =>
|
|
66
|
+
compound(tokens, range) || namedBefore(tokens, range, markers.before) || phraseAfter(tokens, range, markers.after);
|
package/src/document.ts
CHANGED
|
@@ -4,14 +4,21 @@ import { gfmTableFromMarkdown } from "mdast-util-gfm-table";
|
|
|
4
4
|
import { frontmatter } from "micromark-extension-frontmatter";
|
|
5
5
|
import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
|
|
6
6
|
import { atxHeadingText, headingText } from "./heading-text.ts";
|
|
7
|
+
import { hasTitle } from "./heading-title.ts";
|
|
7
8
|
import { maskSpans } from "./mask.ts";
|
|
8
|
-
import { buildTree,
|
|
9
|
+
import { buildTree, type Outline } from "./structure/build.ts";
|
|
9
10
|
import { isMarkdownPath } from "./structure/markdown-path.ts";
|
|
11
|
+
import { pageFurniture, textOutline } from "./page-furniture.ts";
|
|
12
|
+
import { tokenizedLexicons } from "./lexicon-tokens.ts";
|
|
13
|
+
import { inPageAnchors, isInPageNavigation, isNavigationList, type InPageAnchors } from "./in-page-nav.ts";
|
|
10
14
|
import type { BulletList, LanguageAdapter, Paragraph, ProseDocument, Section, Sentence, Span, StructureNode, DocumentProfile } from "./plugin.ts";
|
|
11
15
|
|
|
12
16
|
type Place = { readonly offset?: number | undefined };
|
|
13
17
|
type Node = {
|
|
14
18
|
readonly type: string;
|
|
19
|
+
readonly url?: string | undefined;
|
|
20
|
+
readonly value?: string | undefined;
|
|
21
|
+
readonly identifier?: string | undefined;
|
|
15
22
|
readonly position?: { readonly start: Place; readonly end: Place } | undefined;
|
|
16
23
|
readonly children?: readonly Node[] | undefined;
|
|
17
24
|
};
|
|
@@ -91,14 +98,14 @@ const matchSpans = (source: string, pattern: RegExp): Span[] =>
|
|
|
91
98
|
|
|
92
99
|
const directiveSpans = (source: string): Span[] => [...matchSpans(source, DIRECTIVE), ...matchSpans(source, BARE_URL)];
|
|
93
100
|
|
|
94
|
-
const collectMasks = (root: Node, source: string): Span[] => {
|
|
101
|
+
const collectMasks = (root: Node, source: string, anchors: InPageAnchors): Span[] => {
|
|
95
102
|
const spans: Span[] = [...directiveSpans(source)];
|
|
96
103
|
walk(root, (node) => {
|
|
97
104
|
if (node.type === "link" || node.type === "linkReference") {
|
|
98
105
|
spans.push(...linkChrome(node));
|
|
99
106
|
return;
|
|
100
107
|
}
|
|
101
|
-
if (!NOT_PROSE.has(node.type) && node.type !== "heading") return;
|
|
108
|
+
if (!NOT_PROSE.has(node.type) && node.type !== "heading" && !isInPageNavigation(node, anchors)) return;
|
|
102
109
|
const span = spanOf(node);
|
|
103
110
|
if (span !== undefined) spans.push(span);
|
|
104
111
|
});
|
|
@@ -134,7 +141,8 @@ const headingsOf = (root: Node, source: string): Heading[] => {
|
|
|
134
141
|
if (span === undefined) return;
|
|
135
142
|
// ATX(行頭が #)なら閉じの # も外す。setext の見出しの末尾の # は言葉なので残す。
|
|
136
143
|
const read = source.startsWith("#", span.start) ? atxHeadingText : headingText;
|
|
137
|
-
|
|
144
|
+
const text = read(textOf(node, source));
|
|
145
|
+
if (hasTitle(text)) found.push({ depth, text, start: span.start, end: span.end });
|
|
138
146
|
});
|
|
139
147
|
return found;
|
|
140
148
|
};
|
|
@@ -160,10 +168,10 @@ const strongSpans = (root: Node, masked: readonly Span[]): Span[] => {
|
|
|
160
168
|
|
|
161
169
|
const within = (span: Span, from: number, to: number): boolean => span.start >= from && span.start < to;
|
|
162
170
|
|
|
163
|
-
const spansOfType = (root: Node, type: string): Span[] => {
|
|
171
|
+
const spansOfType = (root: Node, type: string, keep: (node: Node) => boolean = () => true): Span[] => {
|
|
164
172
|
const found: Span[] = [];
|
|
165
173
|
walk(root, (node) => {
|
|
166
|
-
if (node.type !== type) return;
|
|
174
|
+
if (node.type !== type || !keep(node)) return;
|
|
167
175
|
const span = spanOf(node);
|
|
168
176
|
if (span !== undefined) found.push(span);
|
|
169
177
|
});
|
|
@@ -216,11 +224,11 @@ const paragraphsOf = (spans: readonly Span[], sentences: readonly Sentence[], li
|
|
|
216
224
|
.filter((span) => !listSpans.some((list) => span.start >= list.start && span.start < list.end))
|
|
217
225
|
.map((span) => ({ span, sentences: sentences.filter((sentence) => within(sentence.span, span.start, span.end)) }));
|
|
218
226
|
|
|
219
|
-
/** 箇条書きは list ノードの直下の項目を数える。入れ子の項目は内側の list
|
|
220
|
-
const listsOf = (root: Node, source: string): BulletList[] => {
|
|
227
|
+
/** 箇条書きは list ノードの直下の項目を数える。入れ子の項目は内側の list のものとして数える。目次のような案内だけの箇条書きは数えない。 */
|
|
228
|
+
const listsOf = (root: Node, source: string, anchors: InPageAnchors): BulletList[] => {
|
|
221
229
|
const found: BulletList[] = [];
|
|
222
230
|
walk(root, (node) => {
|
|
223
|
-
if (node.type !== "list") return;
|
|
231
|
+
if (node.type !== "list" || isNavigationList(node, anchors)) return;
|
|
224
232
|
const span = spanOf(node);
|
|
225
233
|
if (span === undefined) return;
|
|
226
234
|
const items = (node.children ?? []).flatMap((child) => {
|
|
@@ -261,14 +269,24 @@ export const buildDocument = (
|
|
|
261
269
|
profile: DocumentProfile | undefined = undefined,
|
|
262
270
|
): ProseDocument => {
|
|
263
271
|
const root = parse(source);
|
|
272
|
+
const anchors = inPageAnchors(root);
|
|
264
273
|
// 強調の記号は「本文でないもの」だが、太字の数を数えるときの「覆われた場所」ではない。
|
|
265
274
|
// 同じ集合にすると、太字が自分の記号のせいで覆われた場所にあることになり、1 つも数えられなくなる。
|
|
266
|
-
|
|
275
|
+
// テキストの文書は、ページのヘッダーとフッターも本文ではない(Markdown には改ページが無い)。
|
|
276
|
+
const blocks = [...collectMasks(root, source, anchors), ...(isMarkdownPath(path) ? [] : pageFurniture(source))];
|
|
267
277
|
const masked = [...blocks, ...emphasisSpans(root, source)];
|
|
268
278
|
const prose = maskSpans(source, masked);
|
|
269
|
-
|
|
279
|
+
// ページの案内は段落としても数えない。数えると、目次の行が「本題までの段落」に入る。
|
|
280
|
+
const paragraphSpans = spansOfType(root, "paragraph", (node) => !isInPageNavigation(node, anchors));
|
|
270
281
|
const listItems = spansOfType(root, "listItem");
|
|
271
282
|
const sentences = sentencesOf(prose, paragraphSpans, adapter);
|
|
283
|
+
const lexicons = {
|
|
284
|
+
...adapter.lexicons,
|
|
285
|
+
"internal-jargon": team.jargon.map((pattern) => ({ pattern })),
|
|
286
|
+
// 使わない書き方を pattern に、使う書き方を instead_of に置く。語彙表の「同じことの別の書き方」と同じ向き。
|
|
287
|
+
"preferred-term": Object.entries(team.prefer ?? {}).map(([pattern, use]) => ({ pattern, instead_of: use })),
|
|
288
|
+
};
|
|
289
|
+
const tagged = sentences.some((sentence) => sentence.tokens !== undefined);
|
|
272
290
|
const patterns = adapter.structure;
|
|
273
291
|
// null は「作ったが構造を読めない言語だった」、undefined は「まだ作っていない」。
|
|
274
292
|
const tree: { value: StructureNode | null | undefined } = { value: undefined };
|
|
@@ -282,20 +300,26 @@ export const buildDocument = (
|
|
|
282
300
|
sentences,
|
|
283
301
|
listSpans: listItems,
|
|
284
302
|
paragraphs: paragraphsOf(paragraphSpans, sentences, listItems),
|
|
285
|
-
lists: listsOf(root, source),
|
|
286
|
-
lexicons:
|
|
287
|
-
...adapter.lexicons,
|
|
288
|
-
"internal-jargon": team.jargon.map((pattern) => ({ pattern })),
|
|
289
|
-
// 使わない書き方を pattern に、使う書き方を instead_of に置く。語彙表の「同じことの別の書き方」と同じ向き。
|
|
290
|
-
"preferred-term": Object.entries(team.prefer ?? {}).map(([pattern, use]) => ({ pattern, instead_of: use })),
|
|
291
|
-
},
|
|
303
|
+
lists: listsOf(root, source, anchors),
|
|
304
|
+
lexicons: tagged ? tokenizedLexicons(lexicons, adapter) : lexicons,
|
|
292
305
|
requiredSections: team.requiredSections,
|
|
293
306
|
// 構造の rule(参照先が無い・番号の抜け)が読む木。どの rule も読まなければ作らない。何万行の契約書で、他の rule の lint に代金を払わせない。
|
|
294
307
|
get structure(): StructureNode | undefined {
|
|
295
308
|
tree.value ??=
|
|
296
309
|
patterns === undefined
|
|
297
310
|
? null
|
|
298
|
-
: buildTree(
|
|
311
|
+
: buildTree(
|
|
312
|
+
{
|
|
313
|
+
path,
|
|
314
|
+
source,
|
|
315
|
+
language: adapter.id,
|
|
316
|
+
// テキストの文書は、ページの飾りを覆って読む。フッターの「Section 9」を木の節にしない。
|
|
317
|
+
outline: isMarkdownPath(path) ? outlineOf(root, source) : textOutline(source),
|
|
318
|
+
markdown: isMarkdownPath(path),
|
|
319
|
+
profile,
|
|
320
|
+
},
|
|
321
|
+
patterns,
|
|
322
|
+
);
|
|
299
323
|
return tree.value ?? undefined;
|
|
300
324
|
},
|
|
301
325
|
profile,
|
package/src/eval.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DETECTORS } from "./detectors/index.ts";
|
|
2
2
|
import { charLength } from "./measure.ts";
|
|
3
3
|
import type { ProseDocument, RuleDefinition } from "./plugin.ts";
|
|
4
|
+
import { missingList } from "./declared-lists.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 閾値の較正。
|
|
@@ -48,7 +49,6 @@ const countAt = (docs: readonly ProseDocument[], rule: RuleDefinition, limit: nu
|
|
|
48
49
|
if (detector === undefined) return { limit, findings: 0, documents: 0, per10k: 0 };
|
|
49
50
|
const perDoc = docs.map((doc) => {
|
|
50
51
|
const lexicon = rule.word_list === undefined ? undefined : doc.lexicons[rule.word_list];
|
|
51
|
-
if (rule.word_list !== undefined && lexicon === undefined) return 0;
|
|
52
52
|
return detector(doc, { limit, lexicon, where: rule.where }).length;
|
|
53
53
|
});
|
|
54
54
|
const findings = perDoc.reduce((sum, count) => sum + count, 0);
|
|
@@ -82,6 +82,7 @@ const measurable = (rule: RuleDefinition, docs: readonly ProseDocument[], genre:
|
|
|
82
82
|
if (rule.layer === "L4" || !rule.use_for.some((target) => genre.startsWith(target))) return false;
|
|
83
83
|
if (rule.languages !== undefined && !rule.languages.includes(language)) return false;
|
|
84
84
|
if (rule.from.length > 0) return false;
|
|
85
|
+
if (docs.some((doc) => missingList(rule, doc.lexicons) !== undefined)) return false;
|
|
85
86
|
return rule.requires.every((need) => meets(need, docs));
|
|
86
87
|
};
|
|
87
88
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 文字か数字。記号・空白・絵文字だけの並びには、読者が指せる題が無い。 */
|
|
2
|
+
const TITLE_CHARACTER = /[\p{L}\p{N}]/u;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 見出しに題があるか。`###` だけの行や `## ---` は、変換で残った空の `<h3>` のような区切りで、
|
|
6
|
+
* 本文の始まりでも読者が辿れる節でもない。題の無い見出しは見出しとして数えず、前後の本文を 1 つの節として読む。
|
|
7
|
+
*/
|
|
8
|
+
export const hasTitle = (headingWords: string): boolean => TITLE_CHARACTER.test(headingWords);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** mdast の節のうち、ページ内の案内かどうかの判定に要る部分だけ。 */
|
|
2
|
+
export type NavNode = {
|
|
3
|
+
readonly type: string;
|
|
4
|
+
readonly url?: string | undefined;
|
|
5
|
+
readonly value?: string | undefined;
|
|
6
|
+
readonly identifier?: string | undefined;
|
|
7
|
+
readonly children?: readonly NavNode[] | undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** 参照形式のリンク(`[目次][toc]`)の名前のうち、定義(`[toc]: #目次`)が同じページの中を指すもの。 */
|
|
11
|
+
export type InPageAnchors = ReadonlySet<string>;
|
|
12
|
+
|
|
13
|
+
/** 文字か数字。これが 1 つでもあれば、書いた人の言葉がある。記号(▲ ↑ | ・)だけなら飾り。 */
|
|
14
|
+
const WORD = /[\p{L}\p{N}]/u;
|
|
15
|
+
|
|
16
|
+
const WRAPPERS = new Set(["strong", "emphasis", "delete"]);
|
|
17
|
+
|
|
18
|
+
const pointsInPage = (url: string | undefined): boolean => (url ?? "").startsWith("#");
|
|
19
|
+
|
|
20
|
+
const collectAnchors = (node: NavNode, found: Set<string>): void => {
|
|
21
|
+
if (node.type === "definition" && node.identifier !== undefined && pointsInPage(node.url)) found.add(node.identifier);
|
|
22
|
+
(node.children ?? []).forEach((child) => collectAnchors(child, found));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const inPageAnchors = (root: NavNode): InPageAnchors => {
|
|
26
|
+
const found = new Set<string>();
|
|
27
|
+
collectAnchors(root, found);
|
|
28
|
+
return found;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isInPageLink = (node: NavNode, anchors: InPageAnchors): boolean =>
|
|
32
|
+
(node.type === "link" && pointsInPage(node.url)) || (node.type === "linkReference" && anchors.has(node.identifier ?? ""));
|
|
33
|
+
|
|
34
|
+
const isNavigationPart = (node: NavNode, anchors: InPageAnchors): boolean => {
|
|
35
|
+
if (isInPageLink(node, anchors) || node.type === "break") return true;
|
|
36
|
+
if (node.type === "text") return !WORD.test(node.value ?? "");
|
|
37
|
+
return WRAPPERS.has(node.type) && (node.children ?? []).every((child) => isNavigationPart(child, anchors));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const containsInPageLink = (node: NavNode, anchors: InPageAnchors): boolean =>
|
|
41
|
+
isInPageLink(node, anchors) || (node.children ?? []).some((child) => containsInPageLink(child, anchors));
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 同じページの中へのリンク(`](#…)`)と記号だけでできた段落。「▲ 目次に戻る」や目次の項目で、本文ではなくページの案内。
|
|
45
|
+
* 言葉が 1 つでも混ざれば本文(「詳しくは[第3章](#第3章)を参照」)。他のページへのリンクは書いた人が選んだ一覧なので残す。
|
|
46
|
+
*/
|
|
47
|
+
export const isInPageNavigation = (paragraph: NavNode, anchors: InPageAnchors = new Set()): boolean => {
|
|
48
|
+
const children = paragraph.children ?? [];
|
|
49
|
+
return (
|
|
50
|
+
paragraph.type === "paragraph" &&
|
|
51
|
+
children.every((child) => isNavigationPart(child, anchors)) &&
|
|
52
|
+
children.some((child) => containsInPageLink(child, anchors))
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const isNavigationBlock = (node: NavNode, anchors: InPageAnchors): boolean => isInPageNavigation(node, anchors) || isNavigationList(node, anchors);
|
|
57
|
+
|
|
58
|
+
const isNavigationItem = (item: NavNode, anchors: InPageAnchors): boolean => {
|
|
59
|
+
const children = item.children ?? [];
|
|
60
|
+
return item.type === "listItem" && children.length > 0 && children.every((child) => isNavigationBlock(child, anchors));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** 項目がどれもページの案内(入れ子も含む)の箇条書き。目次の項目の数は節の数で決まり、書いた人が選んだ並べ方ではない。 */
|
|
64
|
+
export const isNavigationList = (list: NavNode, anchors: InPageAnchors = new Set()): boolean => {
|
|
65
|
+
const items = list.children ?? [];
|
|
66
|
+
return list.type === "list" && items.length > 0 && items.every((item) => isNavigationItem(item, anchors));
|
|
67
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { LanguageAdapter, Lexicon, LexiconEntry } from "./plugin.ts";
|
|
2
|
+
|
|
3
|
+
/** adapter の語彙表は adapter ごとに一度だけ分ける。チームの語彙表は文書ごとに作られるので、この表に残らない。 */
|
|
4
|
+
const split = new WeakMap<LanguageAdapter, WeakMap<Lexicon, Lexicon>>();
|
|
5
|
+
|
|
6
|
+
const splitBy = (adapter: LanguageAdapter): WeakMap<Lexicon, Lexicon> => {
|
|
7
|
+
const known = split.get(adapter) ?? new WeakMap<Lexicon, Lexicon>();
|
|
8
|
+
split.set(adapter, known);
|
|
9
|
+
return known;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const tokenized = (entry: LexiconEntry, adapter: LanguageAdapter): LexiconEntry => {
|
|
13
|
+
const tokens = adapter.segment(entry.pattern).sentences.flatMap((sentence) => sentence.tokens ?? []);
|
|
14
|
+
return tokens.length === 0 ? entry : { ...entry, tokens };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const withTokens = (lexicon: Lexicon, adapter: LanguageAdapter): Lexicon => {
|
|
18
|
+
const cache = splitBy(adapter);
|
|
19
|
+
const known = cache.get(lexicon);
|
|
20
|
+
if (known !== undefined) return known;
|
|
21
|
+
const made = lexicon.map((entry) => tokenized(entry, adapter));
|
|
22
|
+
cache.set(lexicon, made);
|
|
23
|
+
return made;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** 語彙表の語を、文と同じ解析器で語に分けておく。語彙表の検査が原形で照らせるように。品詞を読んでいない文書には何もしない。 */
|
|
27
|
+
export const tokenizedLexicons = (lexicons: Readonly<Record<string, Lexicon>>, adapter: LanguageAdapter): Readonly<Record<string, Lexicon>> =>
|
|
28
|
+
Object.fromEntries(Object.entries(lexicons).map(([name, lexicon]) => [name, withTokens(lexicon, adapter)]));
|
package/src/orthography.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 表記のそろい。書き方の決まりのうち、文字だけで決まるもの。
|
|
2
2
|
|
|
3
|
-
const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
3
|
+
export const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
4
4
|
|
|
5
5
|
/** text の中で word が現れる [始まり, 終わり) を、左から順に。大文字小文字を区別しないときは i で探す(位置は元の文字列のまま)。 */
|
|
6
6
|
const spansOf = (text: string, word: string, ignoreCase: boolean): [number, number][] => {
|
|
@@ -67,6 +67,7 @@ const ALPHANUMERIC = /[A-Za-z0-9.]/u;
|
|
|
67
67
|
/**
|
|
68
68
|
* 日本語の左にある英数字の並びの、先頭の字。「3GBの」の「GB」は数量の単位で、並びは数字で始まる。
|
|
69
69
|
* そうした並びと日本語の境目は、英字の空け方ではなく数字の後ろの空け方として数える(「3回」と同じ)。
|
|
70
|
+
* 逆に「H30 等」「EC2 で」は英字で始まる名前で、書き手は英単語と同じに空ける。数字の後ろとしては数えない。
|
|
70
71
|
*/
|
|
71
72
|
const runStart = (chars: readonly string[], last: number): string | undefined => {
|
|
72
73
|
let first = last;
|
|
@@ -74,10 +75,10 @@ const runStart = (chars: readonly string[], last: number): string | undefined =>
|
|
|
74
75
|
return chars[first];
|
|
75
76
|
};
|
|
76
77
|
|
|
77
|
-
/**
|
|
78
|
+
/** 左の並びの種類は先頭の字で決める。数字で始まる並び(3GB、10ms)は数字、英字で始まる並び(H30、v1.2)は英字。 */
|
|
78
79
|
const leftRunBeside = (chars: readonly string[], last: number, japanese: string | undefined): SpacingKind | undefined => {
|
|
79
80
|
const kind = latinBeside(japanese, chars[last], "after-digit");
|
|
80
|
-
return kind ===
|
|
81
|
+
return kind === undefined ? undefined : (kindOf(runStart(chars, last), "after-digit") ?? kind);
|
|
81
82
|
};
|
|
82
83
|
|
|
83
84
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Span } from "./plugin.ts";
|
|
2
|
+
import { NO_OUTLINE, type Outline } from "./structure/build.ts";
|
|
3
|
+
|
|
4
|
+
/** 改ページから、飾りの行を探しにいく行数。飾りと改ページのあいだには空行が数行入る。 */
|
|
5
|
+
const REACH = 3;
|
|
6
|
+
|
|
7
|
+
type Line = { readonly start: number; readonly end: number; readonly text: string };
|
|
8
|
+
|
|
9
|
+
const linesOf = (source: string): Line[] => {
|
|
10
|
+
const scan = { start: 0 };
|
|
11
|
+
return source.split("\n").map((text) => {
|
|
12
|
+
const line = { start: scan.start, end: scan.start + text.length, text };
|
|
13
|
+
scan.start = line.end + 1;
|
|
14
|
+
return line;
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const isBlank = (line: Line | undefined): boolean => line !== undefined && line.text.trim() === "";
|
|
19
|
+
|
|
20
|
+
/** 改ページだけの行。行の途中の \f は本文の一部(写し間違い)で、ページの区切りではない。 */
|
|
21
|
+
const PAGE_BREAK = /^[ \t]*\f[ \t\r]*$/u;
|
|
22
|
+
|
|
23
|
+
/** index から step の向きに、空行を飛ばして最初の空でない行。REACH 行を越えたら探さない。 */
|
|
24
|
+
const nearestText = (lines: readonly Line[], index: number, step: number): Line | undefined => {
|
|
25
|
+
const candidates = Array.from({ length: REACH }, (_, offset) => lines[index + step * (offset + 1)]);
|
|
26
|
+
const skipped = candidates.findIndex((line) => !isBlank(line));
|
|
27
|
+
return skipped === -1 ? undefined : candidates[skipped];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 紙の版を写したテキスト(RFC など)の、ページのヘッダーとフッター。改ページ(\f)の直前と直後の、空でない 1 行ずつ。
|
|
32
|
+
* ページごとに同じ行が出るので、本文として読むと「同じ言い回しの繰り返し」や長い文になる。
|
|
33
|
+
*/
|
|
34
|
+
export const pageFurniture = (source: string): Span[] => {
|
|
35
|
+
const lines = linesOf(source);
|
|
36
|
+
return lines.flatMap((line, index) => {
|
|
37
|
+
if (!PAGE_BREAK.test(line.text)) return [];
|
|
38
|
+
return [nearestText(lines, index, -1), line, nearestText(lines, index, 1)].flatMap((found) =>
|
|
39
|
+
found === undefined ? [] : [{ start: found.start, end: found.end }],
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** テキストの文書の外形。見出しもコードも無く、ページの飾りだけを覆う。木を作る入口はどれもこれを使う。 */
|
|
45
|
+
export const textOutline = (source: string): Outline => ({ ...NO_OUTLINE, opaque: pageFurniture(source) });
|
package/src/plugin.ts
CHANGED
|
@@ -36,7 +36,15 @@ export type LengthUnit = "char" | "word";
|
|
|
36
36
|
* `instead_of` は「同じことを言う別の書きかた」。文体の一貫性を見る rule が使う。
|
|
37
37
|
* 2 つの書きかたのどちらが正しいかは決めず、**1 つの文書で混ざっていないか**だけを見る。
|
|
38
38
|
*/
|
|
39
|
-
export type LexiconEntry = {
|
|
39
|
+
export type LexiconEntry = {
|
|
40
|
+
readonly pattern: string;
|
|
41
|
+
readonly weight?: number | undefined;
|
|
42
|
+
readonly instead_of?: string | undefined;
|
|
43
|
+
/** 語が、かかる語のどちら側に立つか。語順が言語で違うものを語彙表が言う(範囲の「で」は前、"in" は後ろ)。 */
|
|
44
|
+
readonly position?: "before" | "after" | undefined;
|
|
45
|
+
/** pattern を adapter が語に分けたもの。品詞が読めるときだけ core が入れる。語彙表を書く側は書かない。 */
|
|
46
|
+
readonly tokens?: readonly Token[] | undefined;
|
|
47
|
+
};
|
|
40
48
|
|
|
41
49
|
export type Lexicon = readonly LexiconEntry[];
|
|
42
50
|
|
|
@@ -338,11 +346,15 @@ export type RuleDefinition = {
|
|
|
338
346
|
readonly by_genre: Readonly<Record<string, LevelTable>>;
|
|
339
347
|
readonly how_to_find: string;
|
|
340
348
|
readonly word_list: string | undefined;
|
|
349
|
+
/** word_list のほかに detector が名前で引く語彙表。どれかが無い言語では rule を動かさない。 */
|
|
350
|
+
readonly extra_word_lists: readonly string[];
|
|
341
351
|
/** L4 のみ。LLM に渡す決まり。言語別。 */
|
|
342
352
|
readonly what_to_check: Localized | undefined;
|
|
343
353
|
readonly where: string | undefined;
|
|
344
354
|
/** adapter に要る capability。"pos" / "lemma"。満たさなければ動かさない。spec §16。 */
|
|
345
355
|
readonly requires: readonly string[];
|
|
356
|
+
/** 使えるなら用意してほしい capability("pos")。requires と違い、満たせなくても動かす(品詞が無ければ文字だけで見る)。 */
|
|
357
|
+
readonly uses: readonly string[];
|
|
346
358
|
/** 複合シグナル。ここに並べた rule のうち何本が出たかを見る。spec §20.2。 */
|
|
347
359
|
readonly from: readonly string[];
|
|
348
360
|
/** 動かす言語。未指定は全言語。「ですます調」のように言語に固有の rule が使う。 */
|
package/src/render/compact.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { RuleDefinition } from "../plugin.ts";
|
|
2
2
|
import type { RunResult } from "../run.ts";
|
|
3
3
|
import { messageOf } from "./text.ts";
|
|
4
|
+
import { uiLanguageOf, type Texts } from "../ui.ts";
|
|
4
5
|
|
|
5
6
|
/** 「1 finding」「2 findings」。 */
|
|
6
7
|
const counted = (count: number, noun: string): string => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* 最後の集計の行は、画面のほかの部分と同じく文書の言語で出す。行ごとの重さ(warning / error)は、grep や別の道具が読むので英語のまま。
|
|
11
|
+
*/
|
|
12
|
+
const TALLY: Texts<{ readonly findings: (n: number) => string; readonly notRun: (n: number) => string; readonly separator: string }> = {
|
|
13
|
+
ja: { findings: (n) => `指摘 ${String(n)} 件`, notRun: (n) => `動いていない rule ${String(n)} 件`, separator: "、" },
|
|
14
|
+
en: { findings: (n) => counted(n, "finding"), notRun: (n) => `${counted(n, "rule")} not run`, separator: ", " },
|
|
15
|
+
};
|
|
16
|
+
|
|
8
17
|
/** エンジニア向け。1 件 2 行。 */
|
|
9
18
|
export const renderCompact = (header: string, result: RunResult, rules: readonly RuleDefinition[], language: string): string => {
|
|
10
19
|
const byId = new Map(rules.map((rule) => [rule.id, rule]));
|
|
@@ -14,8 +23,9 @@ export const renderCompact = (header: string, result: RunResult, rules: readonly
|
|
|
14
23
|
const at = `${finding.line}:${finding.column}`.padEnd(8);
|
|
15
24
|
return [` ${at}${finding.severity.padEnd(8)}${messageOf(rule, finding, language)}`, ` ${" ".repeat(8)}${finding.rule}`];
|
|
16
25
|
});
|
|
17
|
-
const
|
|
26
|
+
const text = TALLY[uiLanguageOf(language)];
|
|
27
|
+
const tail = [text.findings(result.findings.length), result.skipped.length > 0 ? text.notRun(result.skipped.length) : undefined].filter(
|
|
18
28
|
(part) => part !== undefined,
|
|
19
29
|
);
|
|
20
|
-
return ["", header, "", ...lines, "", tail.join(
|
|
30
|
+
return ["", header, "", ...lines, "", tail.join(text.separator), ""].join("\n");
|
|
21
31
|
};
|
package/src/rule-load.ts
CHANGED
|
@@ -99,9 +99,11 @@ const toRule = (raw: unknown, language: string, file: string): RuleDefinition =>
|
|
|
99
99
|
by_genre: genreTables(raw["by_genre"], language),
|
|
100
100
|
how_to_find: String(raw["how_to_find"]),
|
|
101
101
|
word_list: typeof raw["word_list"] === "string" ? raw["word_list"] : undefined,
|
|
102
|
+
extra_word_lists: stringList(raw["extra_word_lists"]) ?? [],
|
|
102
103
|
what_to_check: isLocalized(raw["what_to_check"]) ? raw["what_to_check"] : undefined,
|
|
103
104
|
where: typeof raw["where"] === "string" ? raw["where"] : undefined,
|
|
104
105
|
requires: stringList(raw["requires"]) ?? [],
|
|
106
|
+
uses: stringList(raw["uses"]) ?? [],
|
|
105
107
|
from: stringList(raw["from"]) ?? [],
|
|
106
108
|
languages: stringList(raw["languages"]),
|
|
107
109
|
use_for: Array.isArray(raw["use_for"]) ? raw["use_for"].map((entry) => String(entry)) : [],
|
package/src/run.ts
CHANGED
|
@@ -4,8 +4,11 @@ import type { AdapterNeeds, Finding, Level, ProseDocument, RuleDefinition } from
|
|
|
4
4
|
import { lineStarts, placeOf } from "./position.ts";
|
|
5
5
|
import { REASONS, type Reasons } from "./reasons.ts";
|
|
6
6
|
import { joinWords } from "./detectors/word-list.ts";
|
|
7
|
+
import { missingList } from "./declared-lists.ts";
|
|
7
8
|
import { unreadStructure, type Unread } from "./structure/unread.ts";
|
|
8
9
|
import { isMarkdownPath } from "./structure/markdown-path.ts";
|
|
10
|
+
import { maskSpans } from "./mask.ts";
|
|
11
|
+
import { textOutline } from "./page-furniture.ts";
|
|
9
12
|
import { uiLanguageOf } from "./ui.ts";
|
|
10
13
|
|
|
11
14
|
export type Skipped = { readonly rule: string; readonly why: string };
|
|
@@ -51,8 +54,9 @@ const treeProblem = (doc: ProseDocument): string | undefined => {
|
|
|
51
54
|
return unread === undefined ? undefined : reasonsFor(doc).unreadStructure(unread.clauses, unread.units);
|
|
52
55
|
};
|
|
53
56
|
|
|
54
|
-
/** 木が読んだのと同じ本文。Markdown はコードを覆ったもの、.txt
|
|
55
|
-
const textTheTreeRead = (doc: ProseDocument): string =>
|
|
57
|
+
/** 木が読んだのと同じ本文。Markdown はコードを覆ったもの、.txt はページの飾りだけを覆ったもの(.txt の字下げはコードではない)。 */
|
|
58
|
+
const textTheTreeRead = (doc: ProseDocument): string =>
|
|
59
|
+
isMarkdownPath(doc.path) ? (doc.prose ?? doc.source) : maskSpans(doc.source, textOutline(doc.source).opaque);
|
|
56
60
|
|
|
57
61
|
const unreadByDocument = new WeakMap<ProseDocument, Unread | undefined>();
|
|
58
62
|
|
|
@@ -104,7 +108,7 @@ export const neededBy = (rules: readonly RuleDefinition[], settings: Settings, e
|
|
|
104
108
|
pos: forGenre(rules, genre)
|
|
105
109
|
.filter((rule) => rule.layer !== "L4" && levelFor(rule, settings, experimental) !== "off")
|
|
106
110
|
.filter((rule) => rule.languages === undefined || rule.languages.includes(language))
|
|
107
|
-
.some((rule) => rule.requires.
|
|
111
|
+
.some((rule) => [...rule.requires, ...rule.uses].some((need) => need === "pos" || need === "lemma")),
|
|
108
112
|
});
|
|
109
113
|
|
|
110
114
|
const place = (starts: readonly number[], finding: Finding): Finding => {
|
|
@@ -178,15 +182,15 @@ export const runRules = (
|
|
|
178
182
|
const detector = DETECTORS[rule.how_to_find];
|
|
179
183
|
if (detector === undefined)
|
|
180
184
|
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noDetector(rule.how_to_find) }] };
|
|
185
|
+
// 語彙表を要求する rule で、その言語に語彙表が無ければ動かせない。黙って通さない。
|
|
186
|
+
const absent = missingList(rule, doc.lexicons);
|
|
187
|
+
if (absent !== undefined)
|
|
188
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noLexicon(doc.language, absent) }] };
|
|
181
189
|
const options = {
|
|
182
190
|
limit: limitFor(rule, level, genre, limits),
|
|
183
191
|
lexicon: rule.word_list === undefined ? undefined : doc.lexicons[rule.word_list],
|
|
184
192
|
where: rule.where,
|
|
185
193
|
};
|
|
186
|
-
// 語彙表を要求する rule で、その言語に語彙表が無ければ動かせない。黙って通さない。
|
|
187
|
-
if (rule.word_list !== undefined && options.lexicon === undefined) {
|
|
188
|
-
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noLexicon(doc.language, rule.word_list) }] };
|
|
189
|
-
}
|
|
190
194
|
const found = detector(doc, options).map((finding) => place(starts, { ...finding, rule: rule.id, severity: rule.severity }));
|
|
191
195
|
return { findings: [...acc.findings, ...found], skipped: acc.skipped };
|
|
192
196
|
},
|
package/src/sentence-shape.ts
CHANGED
|
@@ -37,4 +37,20 @@ const beforeTrailing = (sentence: Sentence): readonly Token[] => {
|
|
|
37
37
|
return kept.length === 0 ? tokens : kept;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
/** 閉じた引用。文がこれで終われば、文末は引用した文のもので、書き手の文末ではない。 */
|
|
41
|
+
const CLOSING_QUOTES = ["」", "』", "\u201d"];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 文末の語: 句読点・終助詞・添えた括弧を除いた後ろから、助動詞の連なりと、その前の述語一つ。
|
|
45
|
+
* 「測定しません」なら「し・ませ・ん」、「十分でしたか」なら「十分・でし・た」。文の途中の引用(「ご覧ください」という)は入らない。
|
|
46
|
+
* 引用で終わる文(画面には「確認します。」。)は、書き手の文末が無いので空。
|
|
47
|
+
*/
|
|
48
|
+
export const endingTokens = (sentence: Sentence): Token[] => {
|
|
49
|
+
const reversed = [...beforeTrailing(sentence)].reverse();
|
|
50
|
+
const last = reversed.findIndex((token) => !SKIP.has(token.pos));
|
|
51
|
+
if (last === -1 || reversed.slice(0, last).some((token) => CLOSING_QUOTES.some((quote) => token.surface.includes(quote)))) return [];
|
|
52
|
+
const head = reversed.findIndex((token, index) => index >= last && token.pos !== "AUX");
|
|
53
|
+
return reversed.slice(last, head === -1 ? reversed.length : head + 1).reverse();
|
|
54
|
+
};
|
|
55
|
+
|
|
40
56
|
export const lastContent = (sentence: Sentence): Token | undefined => [...beforeTrailing(sentence)].reverse().find((token) => !SKIP.has(token.pos));
|