chaffjs 0.9.0 → 0.11.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/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/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/index.d.ts.map +1 -1
- package/dist/detectors/index.js +4 -1
- package/dist/detectors/index.js.map +1 -1
- package/dist/detectors/lexicon-match.d.ts +9 -0
- package/dist/detectors/lexicon-match.d.ts.map +1 -0
- package/dist/detectors/lexicon-match.js +40 -0
- package/dist/detectors/lexicon-match.js.map +1 -0
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +20 -20
- 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 +162 -22
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/structure-tree.d.ts +5 -0
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +50 -1
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/team.d.ts.map +1 -1
- package/dist/detectors/team.js +7 -2
- package/dist/detectors/team.js.map +1 -1
- package/dist/detectors/word-list.d.ts +3 -0
- package/dist/detectors/word-list.d.ts.map +1 -0
- package/dist/detectors/word-list.js +3 -0
- package/dist/detectors/word-list.js.map +1 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +22 -9
- package/dist/document.js.map +1 -1
- 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/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 +15 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/profile/parse.d.ts.map +1 -1
- package/dist/profile/parse.js +15 -2
- package/dist/profile/parse.js.map +1 -1
- package/dist/render/compact.d.ts.map +1 -1
- package/dist/render/compact.js +13 -2
- package/dist/render/compact.js.map +1 -1
- package/dist/rule-load.d.ts.map +1 -1
- package/dist/rule-load.js +1 -0
- package/dist/rule-load.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +22 -8
- 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 +18 -8
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/date-order.d.ts +12 -0
- package/dist/structure/date-order.d.ts.map +1 -0
- package/dist/structure/date-order.js +30 -0
- package/dist/structure/date-order.js.map +1 -0
- 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/relative-find.d.ts +2 -0
- package/dist/structure/relative-find.d.ts.map +1 -1
- package/dist/structure/relative-find.js +11 -0
- package/dist/structure/relative-find.js.map +1 -1
- package/dist/structure/runs.d.ts +20 -0
- package/dist/structure/runs.d.ts.map +1 -0
- package/dist/structure/runs.js +63 -0
- package/dist/structure/runs.js.map +1 -0
- package/dist/structure/total.d.ts +15 -0
- package/dist/structure/total.d.ts.map +1 -0
- package/dist/structure/total.js +143 -0
- package/dist/structure/total.js.map +1 -0
- 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/structure/unnumbered.d.ts +15 -0
- package/dist/structure/unnumbered.d.ts.map +1 -0
- package/dist/structure/unnumbered.js +28 -0
- package/dist/structure/unnumbered.js.map +1 -0
- package/dist/structure/weekday.d.ts +7 -0
- package/dist/structure/weekday.d.ts.map +1 -0
- package/dist/structure/weekday.js +21 -0
- package/dist/structure/weekday.js.map +1 -0
- 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/profiles/statute.yaml +5 -1
- 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/date-order.yaml +26 -0
- package/rules/date-weekday-mismatch.yaml +26 -0
- package/rules/double-keigo.yaml +1 -0
- package/rules/empty-intensifier.yaml +1 -0
- package/rules/excessive-hedging.yaml +1 -0
- package/rules/hiragana-fukushi.yaml +1 -0
- package/rules/internal-jargon.yaml +1 -1
- 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/total-mismatch.yaml +26 -0
- package/rules/unqualified-superlative.yaml +1 -0
- package/src/cli-text.ts +2 -0
- package/src/cli.ts +5 -1
- package/src/detectors/char-shape.ts +38 -3
- package/src/detectors/en-shape.ts +138 -7
- package/src/detectors/gram-place.ts +41 -0
- package/src/detectors/index.ts +4 -1
- package/src/detectors/lexicon-match.ts +44 -0
- package/src/detectors/lexicon.ts +25 -22
- 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 +188 -21
- package/src/detectors/structure-tree.ts +67 -1
- package/src/detectors/team.ts +7 -2
- package/src/detectors/word-list.ts +2 -0
- package/src/document.ts +25 -9
- package/src/lexicon-tokens.ts +28 -0
- package/src/page-furniture.ts +45 -0
- package/src/plugin.ts +24 -3
- package/src/profile/parse.ts +15 -2
- package/src/render/compact.ts +15 -2
- package/src/rule-load.ts +1 -0
- package/src/run.ts +24 -8
- package/src/sentence-shape.ts +16 -0
- package/src/structure/bare-numbers.ts +78 -0
- package/src/structure/build.ts +27 -9
- package/src/structure/date-order.ts +44 -0
- package/src/structure/issues.ts +16 -2
- package/src/structure/of.ts +4 -2
- package/src/structure/relative-find.ts +13 -0
- package/src/structure/runs.ts +71 -0
- package/src/structure/total.ts +183 -0
- package/src/structure/universal.ts +14 -3
- package/src/structure/unnumbered.ts +33 -0
- package/src/structure/weekday.ts +25 -0
- package/src/version.ts +21 -3
package/src/document.ts
CHANGED
|
@@ -5,8 +5,10 @@ import { frontmatter } from "micromark-extension-frontmatter";
|
|
|
5
5
|
import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
|
|
6
6
|
import { atxHeadingText, headingText } from "./heading-text.ts";
|
|
7
7
|
import { maskSpans } from "./mask.ts";
|
|
8
|
-
import { buildTree,
|
|
8
|
+
import { buildTree, type Outline } from "./structure/build.ts";
|
|
9
9
|
import { isMarkdownPath } from "./structure/markdown-path.ts";
|
|
10
|
+
import { pageFurniture, textOutline } from "./page-furniture.ts";
|
|
11
|
+
import { tokenizedLexicons } from "./lexicon-tokens.ts";
|
|
10
12
|
import type { BulletList, LanguageAdapter, Paragraph, ProseDocument, Section, Sentence, Span, StructureNode, DocumentProfile } from "./plugin.ts";
|
|
11
13
|
|
|
12
14
|
type Place = { readonly offset?: number | undefined };
|
|
@@ -263,12 +265,20 @@ export const buildDocument = (
|
|
|
263
265
|
const root = parse(source);
|
|
264
266
|
// 強調の記号は「本文でないもの」だが、太字の数を数えるときの「覆われた場所」ではない。
|
|
265
267
|
// 同じ集合にすると、太字が自分の記号のせいで覆われた場所にあることになり、1 つも数えられなくなる。
|
|
266
|
-
|
|
268
|
+
// テキストの文書は、ページのヘッダーとフッターも本文ではない(Markdown には改ページが無い)。
|
|
269
|
+
const blocks = [...collectMasks(root, source), ...(isMarkdownPath(path) ? [] : pageFurniture(source))];
|
|
267
270
|
const masked = [...blocks, ...emphasisSpans(root, source)];
|
|
268
271
|
const prose = maskSpans(source, masked);
|
|
269
272
|
const paragraphSpans = spansOfType(root, "paragraph");
|
|
270
273
|
const listItems = spansOfType(root, "listItem");
|
|
271
274
|
const sentences = sentencesOf(prose, paragraphSpans, adapter);
|
|
275
|
+
const lexicons = {
|
|
276
|
+
...adapter.lexicons,
|
|
277
|
+
"internal-jargon": team.jargon.map((pattern) => ({ pattern })),
|
|
278
|
+
// 使わない書き方を pattern に、使う書き方を instead_of に置く。語彙表の「同じことの別の書き方」と同じ向き。
|
|
279
|
+
"preferred-term": Object.entries(team.prefer ?? {}).map(([pattern, use]) => ({ pattern, instead_of: use })),
|
|
280
|
+
};
|
|
281
|
+
const tagged = sentences.some((sentence) => sentence.tokens !== undefined);
|
|
272
282
|
const patterns = adapter.structure;
|
|
273
283
|
// null は「作ったが構造を読めない言語だった」、undefined は「まだ作っていない」。
|
|
274
284
|
const tree: { value: StructureNode | null | undefined } = { value: undefined };
|
|
@@ -283,19 +293,25 @@ export const buildDocument = (
|
|
|
283
293
|
listSpans: listItems,
|
|
284
294
|
paragraphs: paragraphsOf(paragraphSpans, sentences, listItems),
|
|
285
295
|
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
|
-
},
|
|
296
|
+
lexicons: tagged ? tokenizedLexicons(lexicons, adapter) : lexicons,
|
|
292
297
|
requiredSections: team.requiredSections,
|
|
293
298
|
// 構造の rule(参照先が無い・番号の抜け)が読む木。どの rule も読まなければ作らない。何万行の契約書で、他の rule の lint に代金を払わせない。
|
|
294
299
|
get structure(): StructureNode | undefined {
|
|
295
300
|
tree.value ??=
|
|
296
301
|
patterns === undefined
|
|
297
302
|
? null
|
|
298
|
-
: buildTree(
|
|
303
|
+
: buildTree(
|
|
304
|
+
{
|
|
305
|
+
path,
|
|
306
|
+
source,
|
|
307
|
+
language: adapter.id,
|
|
308
|
+
// テキストの文書は、ページの飾りを覆って読む。フッターの「Section 9」を木の節にしない。
|
|
309
|
+
outline: isMarkdownPath(path) ? outlineOf(root, source) : textOutline(source),
|
|
310
|
+
markdown: isMarkdownPath(path),
|
|
311
|
+
profile,
|
|
312
|
+
},
|
|
313
|
+
patterns,
|
|
314
|
+
);
|
|
299
315
|
return tree.value ?? undefined;
|
|
300
316
|
},
|
|
301
317
|
profile,
|
|
@@ -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)]));
|
|
@@ -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,13 @@ 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
|
+
/** pattern を adapter が語に分けたもの。品詞が読めるときだけ core が入れる。語彙表を書く側は書かない。 */
|
|
44
|
+
readonly tokens?: readonly Token[] | undefined;
|
|
45
|
+
};
|
|
40
46
|
|
|
41
47
|
export type Lexicon = readonly LexiconEntry[];
|
|
42
48
|
|
|
@@ -213,6 +219,11 @@ export type DocumentProfile = {
|
|
|
213
219
|
readonly caption?: string | undefined;
|
|
214
220
|
/** 前条・同項のように、書いた場所から番地が決まる参照の読み方。無ければ読まない。 */
|
|
215
221
|
readonly relative?: RelativeVocabulary | undefined;
|
|
222
|
+
/**
|
|
223
|
+
* 番号を書かない単位(古い法令の第 2 項以降)。indent で始まる行を、inside の種類の単位の中の、depth の深さの次の単位にする。
|
|
224
|
+
* inside の単位の行に、番号の後ろの本文が続くときだけ。
|
|
225
|
+
*/
|
|
226
|
+
readonly unnumbered?: { readonly indent: string; readonly inside: string; readonly depth: number } | undefined;
|
|
216
227
|
};
|
|
217
228
|
|
|
218
229
|
/**
|
|
@@ -239,9 +250,17 @@ export type RelativeVocabulary = {
|
|
|
239
250
|
readonly notAfter: string | undefined;
|
|
240
251
|
/**
|
|
241
252
|
* 読み替え(「『前条』とあるのは『…』」)の括弧。中の前条・同項は読み替える先の文の言葉で、ここからは決められないので読まない。
|
|
242
|
-
* 閉じの直後に after、開きの直前に before
|
|
253
|
+
* 閉じの直後に after、開きの直前に before のどれかがあるものだけ。中の番地を名指しした参照は、document の名前の文書を指す。
|
|
243
254
|
*/
|
|
244
|
-
readonly substitution:
|
|
255
|
+
readonly substitution:
|
|
256
|
+
| {
|
|
257
|
+
readonly open: string;
|
|
258
|
+
readonly close: string;
|
|
259
|
+
readonly after: readonly string[];
|
|
260
|
+
readonly before: readonly string[];
|
|
261
|
+
readonly document: string | undefined;
|
|
262
|
+
}
|
|
263
|
+
| undefined;
|
|
245
264
|
/** 条を書かずにこの単位で始まる番地(法令の「第一項」「第二号」)は、書いた場所を含むまとまりの中を指す。 */
|
|
246
265
|
readonly inside: readonly string[];
|
|
247
266
|
/**
|
|
@@ -330,6 +349,8 @@ export type RuleDefinition = {
|
|
|
330
349
|
readonly where: string | undefined;
|
|
331
350
|
/** adapter に要る capability。"pos" / "lemma"。満たさなければ動かさない。spec §16。 */
|
|
332
351
|
readonly requires: readonly string[];
|
|
352
|
+
/** 使えるなら用意してほしい capability("pos")。requires と違い、満たせなくても動かす(品詞が無ければ文字だけで見る)。 */
|
|
353
|
+
readonly uses: readonly string[];
|
|
333
354
|
/** 複合シグナル。ここに並べた rule のうち何本が出たかを見る。spec §20.2。 */
|
|
334
355
|
readonly from: readonly string[];
|
|
335
356
|
/** 動かす言語。未指定は全言語。「ですます調」のように言語に固有の rule が使う。 */
|
package/src/profile/parse.ts
CHANGED
|
@@ -41,7 +41,16 @@ const substitutionOf = (value: unknown): RelativeVocabulary["substitution"] => {
|
|
|
41
41
|
if (!isRecord(value)) return undefined;
|
|
42
42
|
const [open, close] = [text(value["open"]), text(value["close"])];
|
|
43
43
|
const [after, before] = [strings(value["after"]), strings(value["before"])];
|
|
44
|
-
return open === undefined || close === undefined || after.length + before.length === 0
|
|
44
|
+
return open === undefined || close === undefined || after.length + before.length === 0
|
|
45
|
+
? undefined
|
|
46
|
+
: { open, close, after, before, document: text(value["document"]) };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** 字下げと、どの種類の中か、どの深さかのどれかが無ければ読まない。 */
|
|
50
|
+
const unnumberedOf = (value: unknown): DocumentProfile["unnumbered"] => {
|
|
51
|
+
if (!isRecord(value)) return undefined;
|
|
52
|
+
const [indent, inside, depth] = [text(value["indent"]), text(value["inside"]), value["depth"]];
|
|
53
|
+
return indent === undefined || inside === undefined || !Number.isInteger(depth) || typeof depth !== "number" ? undefined : { indent, inside, depth };
|
|
45
54
|
};
|
|
46
55
|
|
|
47
56
|
/** 単位が一つも無ければ読まない。何を指すのか決められない。 */
|
|
@@ -78,7 +87,11 @@ const languagesOf = (id: string, raw: Record<string, unknown>): Record<string, D
|
|
|
78
87
|
const addressEnd = text(section["address_end"]);
|
|
79
88
|
const caption = text(section["caption"]);
|
|
80
89
|
const relative = relativeOf(section["relative"]);
|
|
81
|
-
|
|
90
|
+
const unnumbered = unnumberedOf(section["unnumbered"]);
|
|
91
|
+
return [
|
|
92
|
+
language,
|
|
93
|
+
{ id, addresses: strings(section["addresses"]), connectives: strings(section["connectives"]), addressEnd, caption, relative, unnumbered },
|
|
94
|
+
];
|
|
82
95
|
}),
|
|
83
96
|
);
|
|
84
97
|
|
package/src/render/compact.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
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";
|
|
5
|
+
|
|
6
|
+
/** 「1 finding」「2 findings」。 */
|
|
7
|
+
const counted = (count: number, noun: string): string => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
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
|
+
};
|
|
4
16
|
|
|
5
17
|
/** エンジニア向け。1 件 2 行。 */
|
|
6
18
|
export const renderCompact = (header: string, result: RunResult, rules: readonly RuleDefinition[], language: string): string => {
|
|
@@ -11,8 +23,9 @@ export const renderCompact = (header: string, result: RunResult, rules: readonly
|
|
|
11
23
|
const at = `${finding.line}:${finding.column}`.padEnd(8);
|
|
12
24
|
return [` ${at}${finding.severity.padEnd(8)}${messageOf(rule, finding, language)}`, ` ${" ".repeat(8)}${finding.rule}`];
|
|
13
25
|
});
|
|
14
|
-
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(
|
|
15
28
|
(part) => part !== undefined,
|
|
16
29
|
);
|
|
17
|
-
return ["", header, "", ...lines, "", tail.join(
|
|
30
|
+
return ["", header, "", ...lines, "", tail.join(text.separator), ""].join("\n");
|
|
18
31
|
};
|
package/src/rule-load.ts
CHANGED
|
@@ -102,6 +102,7 @@ const toRule = (raw: unknown, language: string, file: string): RuleDefinition =>
|
|
|
102
102
|
what_to_check: isLocalized(raw["what_to_check"]) ? raw["what_to_check"] : undefined,
|
|
103
103
|
where: typeof raw["where"] === "string" ? raw["where"] : undefined,
|
|
104
104
|
requires: stringList(raw["requires"]) ?? [],
|
|
105
|
+
uses: stringList(raw["uses"]) ?? [],
|
|
105
106
|
from: stringList(raw["from"]) ?? [],
|
|
106
107
|
languages: stringList(raw["languages"]),
|
|
107
108
|
use_for: Array.isArray(raw["use_for"]) ? raw["use_for"].map((entry) => String(entry)) : [],
|
package/src/run.ts
CHANGED
|
@@ -3,8 +3,11 @@ import { resolve } from "./levels.ts";
|
|
|
3
3
|
import type { AdapterNeeds, Finding, Level, ProseDocument, RuleDefinition } from "./plugin.ts";
|
|
4
4
|
import { lineStarts, placeOf } from "./position.ts";
|
|
5
5
|
import { REASONS, type Reasons } from "./reasons.ts";
|
|
6
|
+
import { joinWords } from "./detectors/word-list.ts";
|
|
6
7
|
import { unreadStructure, type Unread } from "./structure/unread.ts";
|
|
7
8
|
import { isMarkdownPath } from "./structure/markdown-path.ts";
|
|
9
|
+
import { maskSpans } from "./mask.ts";
|
|
10
|
+
import { textOutline } from "./page-furniture.ts";
|
|
8
11
|
import { uiLanguageOf } from "./ui.ts";
|
|
9
12
|
|
|
10
13
|
export type Skipped = { readonly rule: string; readonly why: string };
|
|
@@ -32,14 +35,27 @@ const reasonsFor = (doc: ProseDocument): Reasons => REASONS[uiLanguageOf(doc.lan
|
|
|
32
35
|
* 構造の rule が動けない理由。木を作れない言語か、番号の行を読めなかった文書。
|
|
33
36
|
* 読めなかった文書で「参照先が無い」が 0 件なのは、確かめた結果ではないので、読めなかったと言う。
|
|
34
37
|
*/
|
|
38
|
+
/**
|
|
39
|
+
* 木を読む rule の要求。structure は番号の並びを読む(読めなかった文書では動かない)。dates と quantities は木の日付と数量だけを
|
|
40
|
+
* 読むので、番号を読めなかった文書でも動く。どちらも capability ではなく、adapter が木を作れるかで決まる。
|
|
41
|
+
*/
|
|
42
|
+
const TREE_NEEDS: ReadonlySet<string> = new Set(["structure", "dates", "quantities"]);
|
|
43
|
+
const VALUE_NEEDS: readonly string[] = ["dates", "quantities"];
|
|
44
|
+
|
|
45
|
+
const treeNeed = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
46
|
+
if (rule.requires.includes("structure")) return treeProblem(doc);
|
|
47
|
+
return rule.requires.some((need) => VALUE_NEEDS.includes(need)) && doc.structure === undefined ? reasonsFor(doc).noStructure(doc.language) : undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
35
50
|
const treeProblem = (doc: ProseDocument): string | undefined => {
|
|
36
51
|
if (doc.structure === undefined) return reasonsFor(doc).noStructure(doc.language);
|
|
37
52
|
const unread = unreadOf(doc);
|
|
38
53
|
return unread === undefined ? undefined : reasonsFor(doc).unreadStructure(unread.clauses, unread.units);
|
|
39
54
|
};
|
|
40
55
|
|
|
41
|
-
/** 木が読んだのと同じ本文。Markdown はコードを覆ったもの、.txt
|
|
42
|
-
const textTheTreeRead = (doc: ProseDocument): string =>
|
|
56
|
+
/** 木が読んだのと同じ本文。Markdown はコードを覆ったもの、.txt はページの飾りだけを覆ったもの(.txt の字下げはコードではない)。 */
|
|
57
|
+
const textTheTreeRead = (doc: ProseDocument): string =>
|
|
58
|
+
isMarkdownPath(doc.path) ? (doc.prose ?? doc.source) : maskSpans(doc.source, textOutline(doc.source).opaque);
|
|
43
59
|
|
|
44
60
|
const unreadByDocument = new WeakMap<ProseDocument, Unread | undefined>();
|
|
45
61
|
|
|
@@ -68,7 +84,7 @@ const hasTokens = (doc: ProseDocument): boolean => doc.sentences.length === 0 ||
|
|
|
68
84
|
/** 要求を満たさない rule は動かせない。満たさないまま動かすと「指摘 0 件」が保証に見える。 */
|
|
69
85
|
const unmet = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
70
86
|
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return reasonsFor(doc).otherLanguage(doc.language);
|
|
71
|
-
const missing = rule.requires.filter((need) => need
|
|
87
|
+
const missing = rule.requires.filter((need) => !TREE_NEEDS.has(need)).find((need) => !has(doc.capabilities, need));
|
|
72
88
|
if (missing !== undefined) return reasonsFor(doc).noCapability(missing);
|
|
73
89
|
return undefined;
|
|
74
90
|
};
|
|
@@ -91,7 +107,7 @@ export const neededBy = (rules: readonly RuleDefinition[], settings: Settings, e
|
|
|
91
107
|
pos: forGenre(rules, genre)
|
|
92
108
|
.filter((rule) => rule.layer !== "L4" && levelFor(rule, settings, experimental) !== "off")
|
|
93
109
|
.filter((rule) => rule.languages === undefined || rule.languages.includes(language))
|
|
94
|
-
.some((rule) => rule.requires.
|
|
110
|
+
.some((rule) => [...rule.requires, ...rule.uses].some((need) => need === "pos" || need === "lemma")),
|
|
95
111
|
});
|
|
96
112
|
|
|
97
113
|
const place = (starts: readonly number[], finding: Finding): Finding => {
|
|
@@ -107,7 +123,7 @@ const place = (starts: readonly number[], finding: Finding): Finding => {
|
|
|
107
123
|
* 元の指摘は消さない。`padded-intro` のように単独でも正しい指摘が混ざっており、
|
|
108
124
|
* まとめるために消すと本物の指摘が見えなくなる。
|
|
109
125
|
*/
|
|
110
|
-
const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: number, starts: readonly number[]): Finding[] => {
|
|
126
|
+
const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: number, starts: readonly number[], language: string): Finding[] => {
|
|
111
127
|
const fired = rule.from.filter((id) => found.some((finding) => finding.rule === id));
|
|
112
128
|
if (fired.length < limit) return [];
|
|
113
129
|
const first = found.find((finding) => fired.includes(finding.rule));
|
|
@@ -118,7 +134,7 @@ const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: num
|
|
|
118
134
|
line: 0,
|
|
119
135
|
column: 0,
|
|
120
136
|
quote: first?.quote ?? "",
|
|
121
|
-
values: { word: fired
|
|
137
|
+
values: { word: joinWords(fired, language), count: fired.length, limit, offset: first === undefined ? 0 : Number(first.values["offset"] ?? 0) },
|
|
122
138
|
}),
|
|
123
139
|
];
|
|
124
140
|
};
|
|
@@ -158,7 +174,7 @@ export const runRules = (
|
|
|
158
174
|
if (noTags !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTags }] };
|
|
159
175
|
// 木は capability ではなく、adapter が structure を持つかで決まる。持たない言語で動かすと「参照先が無い」が 0 件に見える。
|
|
160
176
|
// 段階を見た後で聞く。doc.structure は触れたときに木を作るので、止めている rule のために作らない。
|
|
161
|
-
const noTree = rule
|
|
177
|
+
const noTree = treeNeed(rule, doc);
|
|
162
178
|
if (noTree !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTree }] };
|
|
163
179
|
// 複合シグナルは二段目で扱う。一段目では「検出器が無い」と言わせない。
|
|
164
180
|
if (rule.from.length > 0) return acc;
|
|
@@ -181,7 +197,7 @@ export const runRules = (
|
|
|
181
197
|
);
|
|
182
198
|
const composites = applicable
|
|
183
199
|
.filter((rule) => rule.from.length > 0 && levelFor(rule, settings, experimental) !== "off")
|
|
184
|
-
.flatMap((rule) => compositeOf(rule, outcome.findings, limitFor(rule, levelFor(rule, settings, experimental), genre, limits), starts));
|
|
200
|
+
.flatMap((rule) => compositeOf(rule, outcome.findings, limitFor(rule, levelFor(rule, settings, experimental), genre, limits), starts, doc.language));
|
|
185
201
|
const all = [...outcome.findings, ...composites];
|
|
186
202
|
return { ...outcome, findings: [...all].sort((left, right) => left.line - right.line), forcedExperimental: forced };
|
|
187
203
|
};
|
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));
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Line } from "./runs.ts";
|
|
2
|
+
|
|
3
|
+
/** 単位を書かない数。単位は表の列の見出しにあるか、無い。木の数量は必ず単位を持つので、空の単位とは重ならない。 */
|
|
4
|
+
export const NO_UNIT = "";
|
|
5
|
+
|
|
6
|
+
export type BareNumber = { readonly offset: number; readonly end: number; readonly value: number; readonly unit: typeof NO_UNIT };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 表の升に数だけを書いたもの(1,205、12.5、-50、▲50、**1,205**)。前の印と強調は数に入れない。
|
|
10
|
+
* 0 で始まる番号(007)、区切りの崩れた 1,20、括弧の (50)、注の付いた 1,205a は数にしない。
|
|
11
|
+
*/
|
|
12
|
+
const BARE_NUMBER = /^(?:[1-9]\d{0,2}(?:,\d{3})+|[1-9]\d*|0)(?:\.\d+)?$/u;
|
|
13
|
+
|
|
14
|
+
/** 数の前の空白、強調、負の印(-、▲)と、数の後ろの強調と空白。 */
|
|
15
|
+
const BEFORE_NUMBER = /^[ \t]*[*_]*(?:[-−▲△][ \t]?)?/u;
|
|
16
|
+
const AFTER_NUMBER = /^[*_]*\s*$/u;
|
|
17
|
+
const DIGITS = /^[\d,.]*/u;
|
|
18
|
+
|
|
19
|
+
/** 表の列の区切り。\| は列の中に書いた | なので数えない。 */
|
|
20
|
+
export const CELL_SEPARATOR = /(?<!\\)\|/gu;
|
|
21
|
+
|
|
22
|
+
type CellSpan = { readonly start: number; readonly end: number };
|
|
23
|
+
|
|
24
|
+
const cellsOf = (source: string, line: Line): CellSpan[] => {
|
|
25
|
+
const text = source.slice(line.start, line.end);
|
|
26
|
+
const bounds = [-1, ...[...text.matchAll(CELL_SEPARATOR)].map((match) => match.index), text.length];
|
|
27
|
+
return bounds.slice(1).map((end, index) => ({ start: line.start + (bounds[index] ?? 0) + 1, end: line.start + end }));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type InColumn = BareNumber & { readonly column: number; readonly digits: string };
|
|
31
|
+
|
|
32
|
+
const bareNumberIn = (source: string, cell: CellSpan, column: number): InColumn[] => {
|
|
33
|
+
const text = source.slice(cell.start, cell.end);
|
|
34
|
+
const before = BEFORE_NUMBER.exec(text)?.[0] ?? "";
|
|
35
|
+
const rest = text.slice(before.length);
|
|
36
|
+
const digits = DIGITS.exec(rest)?.[0] ?? "";
|
|
37
|
+
if (!BARE_NUMBER.test(digits) || !AFTER_NUMBER.test(rest.slice(digits.length))) return [];
|
|
38
|
+
const offset = cell.start + before.length;
|
|
39
|
+
return [{ offset, end: offset + digits.length, value: Number(digits.replace(/,/gu, "")), unit: NO_UNIT, column, digits }];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** 表の見出し。最初の行の上の区切り行(|---|)の、さらに上の行。 */
|
|
43
|
+
const headerAbove = (source: string, firstRow: Line | undefined): string => {
|
|
44
|
+
if (firstRow === undefined || firstRow.start < 2) return "";
|
|
45
|
+
const ruleStart = source.lastIndexOf("\n", firstRow.start - 2) + 1;
|
|
46
|
+
if (ruleStart < 2) return "";
|
|
47
|
+
return source.slice(source.lastIndexOf("\n", ruleStart - 2) + 1, ruleStart - 1);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** 見出しに % のある列は率。 */
|
|
51
|
+
const PERCENT = /[%%]/u;
|
|
52
|
+
|
|
53
|
+
/** 同じ桁数で 4 桁以上の、区切りの無い整数だけの列は、年か番号(2024、1041)。 */
|
|
54
|
+
const YEAR_OR_CODE = /^\d{4,}$/u;
|
|
55
|
+
|
|
56
|
+
const isYearsOrCodes = (numbers: readonly InColumn[]): boolean =>
|
|
57
|
+
numbers.every((number) => YEAR_OR_CODE.test(number.digits) && number.digits.length === numbers[0]?.digits.length);
|
|
58
|
+
|
|
59
|
+
/** 足してよい列か。見出しが率を言う列と、年や番号の列は足さない。 */
|
|
60
|
+
const isAmountColumn = (header: string, numbers: readonly InColumn[], column: number): boolean =>
|
|
61
|
+
!PERCENT.test(header.split(CELL_SEPARATOR)[column] ?? "") && !isYearsOrCodes(numbers);
|
|
62
|
+
|
|
63
|
+
/** 表の行の、数だけを書いた升。箇条書きの行は読まない。 */
|
|
64
|
+
export const bareNumbersIn = (source: string, lines: readonly Line[]): BareNumber[] => {
|
|
65
|
+
const rows = lines.filter((line) => line.kind === "table");
|
|
66
|
+
const found = rows.flatMap((line) => cellsOf(source, line).flatMap((cell, column) => bareNumberIn(source, cell, column)));
|
|
67
|
+
const header = headerAbove(source, rows[0]);
|
|
68
|
+
const amountColumns = new Set(
|
|
69
|
+
[...new Set(found.map((number) => number.column))].filter((column) =>
|
|
70
|
+
isAmountColumn(
|
|
71
|
+
header,
|
|
72
|
+
found.filter((number) => number.column === column),
|
|
73
|
+
column,
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
return found.filter((number) => amountColumns.has(number.column)).map(({ offset, end, value, unit }) => ({ offset, end, value, unit }));
|
|
78
|
+
};
|
package/src/structure/build.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Heading } from "../document.ts";
|
|
2
2
|
import { atxHeadingText, headingText } from "../heading-text.ts";
|
|
3
3
|
import { addressSpans } from "../address-chain.ts";
|
|
4
|
-
import { relativeMentions } from "./relative-find.ts";
|
|
4
|
+
import { relativeMentions, substitutedDocuments } from "./relative-find.ts";
|
|
5
5
|
import { resolveRelative } from "./relative-resolve.ts";
|
|
6
6
|
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 { unnumberedUnit, type OpenUnit } from "./unnumbered.ts";
|
|
10
11
|
|
|
11
12
|
/** 組み立て中の節点。できあがったら StructureNode に固める。 */
|
|
12
13
|
type Draft = {
|
|
@@ -24,7 +25,7 @@ type Draft = {
|
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
/** 開いている節点。rank の大きいものほど内側。 */
|
|
27
|
-
type Frame = { readonly draft: Draft; readonly rank: number; readonly numbered?: NumberedLine | undefined };
|
|
28
|
+
type Frame = { readonly draft: Draft; readonly rank: number; readonly numbered?: NumberedLine | undefined; readonly bodyOnLine?: boolean };
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* 見出しは深さ 1〜6 を rank にする。番号のまとまりは必ず見出しの内側に入るので、
|
|
@@ -43,6 +44,7 @@ type State = {
|
|
|
43
44
|
readonly profile: DocumentProfile | undefined;
|
|
44
45
|
/** 見出しだけの行(文書の種類の caption)の行番号と、その言葉。すぐ次の行の条が見出しに使う。 */
|
|
45
46
|
readonly captions: Map<number, string>;
|
|
47
|
+
readonly plainText: boolean;
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
const draftOf = (kind: StructureKind, address: string, line: Line, attrs: Readonly<Record<string, string | number>>): Draft => ({
|
|
@@ -96,7 +98,7 @@ const headingAddress = (state: State, depth: number): string => {
|
|
|
96
98
|
* 見出しに書いた番号(「## 第3条」)は、見出しの深さで入れ子にする。本文の番号は見出しの内側に入る。
|
|
97
99
|
* 見出しの深さを捨てると、「## 第3条」の下の「### 詳細」が条の外に出てしまう。
|
|
98
100
|
*/
|
|
99
|
-
const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingDepth: number | undefined): void => {
|
|
101
|
+
const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingDepth: number | undefined, bodyOnLine: boolean): void => {
|
|
100
102
|
const attrs = { label: numbered.label, ...(numbered.heading === "" ? {} : { heading: numbered.heading }) };
|
|
101
103
|
const rank = headingDepth ?? NUMBERED_RANK + numbered.depth;
|
|
102
104
|
// 番地は、自分と同じか内側を閉じてから決める。閉じる前だと、同じ深さの前の条を親と取り違える。
|
|
@@ -108,7 +110,7 @@ const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingD
|
|
|
108
110
|
ordinalTo: numbered.ordinalTo,
|
|
109
111
|
numbering: numbered.numbering,
|
|
110
112
|
};
|
|
111
|
-
open(state, { draft, rank, numbered });
|
|
113
|
+
open(state, { draft, rank, numbered, bodyOnLine });
|
|
112
114
|
};
|
|
113
115
|
|
|
114
116
|
const openSection = (state: State, line: Line, heading: Heading): void => {
|
|
@@ -130,7 +132,7 @@ type LeafFinder = (patterns: StructurePatterns, text: string, profile: DocumentP
|
|
|
130
132
|
|
|
131
133
|
const LEAVES: readonly { readonly kind: StructureKind; readonly find: LeafFinder }[] = [
|
|
132
134
|
{ kind: "definition", find: (patterns, text) => patterns.definitions(text) },
|
|
133
|
-
{ kind: "reference", find: (patterns, text) => patterns.references(text) },
|
|
135
|
+
{ kind: "reference", find: (patterns, text, profile) => substitutedDocuments(patterns.references(text), text, profile) },
|
|
134
136
|
{
|
|
135
137
|
kind: "reference",
|
|
136
138
|
find: (patterns, text, profile) => {
|
|
@@ -144,8 +146,8 @@ const LEAVES: readonly { readonly kind: StructureKind; readonly find: LeafFinder
|
|
|
144
146
|
];
|
|
145
147
|
|
|
146
148
|
/** 言語を問わない通し番号。後ろが単位なら数量なので番号にしない。 */
|
|
147
|
-
const universalNumber = (patterns: StructurePatterns, text: string, context: NumberingContext): NumberedLine | undefined => {
|
|
148
|
-
const dotted = dottedNumber(text, context);
|
|
149
|
+
const universalNumber = (patterns: StructurePatterns, text: string, context: NumberingContext, plainText: boolean): NumberedLine | undefined => {
|
|
150
|
+
const dotted = dottedNumber(text, context, plainText);
|
|
149
151
|
return dotted !== undefined && patterns.countedAfter?.(dotted.number, dotted.rest) === true ? undefined : dotted;
|
|
150
152
|
};
|
|
151
153
|
|
|
@@ -230,6 +232,8 @@ export type StructureInput = {
|
|
|
230
232
|
readonly source: string;
|
|
231
233
|
readonly language: string;
|
|
232
234
|
readonly outline: Outline;
|
|
235
|
+
/** Markdown か、テキストか。テキストの仕様書だけの書き方(「5. Security Considerations」)がある。 */
|
|
236
|
+
readonly markdown: boolean;
|
|
233
237
|
readonly profile?: DocumentProfile | undefined;
|
|
234
238
|
};
|
|
235
239
|
|
|
@@ -265,6 +269,18 @@ const captionOf = (profile: DocumentProfile | undefined, text: string): string |
|
|
|
265
269
|
return words === "" ? undefined : words;
|
|
266
270
|
};
|
|
267
271
|
|
|
272
|
+
const openUnits = (state: State): OpenUnit[] =>
|
|
273
|
+
state.stack.flatMap((frame) => (frame.numbered === undefined ? [] : [{ numbered: frame.numbered, bodyOnLine: frame.bodyOnLine === true }]));
|
|
274
|
+
|
|
275
|
+
/** 言語の番号の読み方、文書の種類の番号を書かない単位、言語を問わない通し番号の順に読む。 */
|
|
276
|
+
const numberedLine = (state: State, patterns: StructurePatterns, text: string, context: NumberingContext): NumberedLine | undefined =>
|
|
277
|
+
patterns.numbered(text, context) ??
|
|
278
|
+
unnumberedUnit(text, openUnits(state), state.profile?.unnumbered) ??
|
|
279
|
+
universalNumber(patterns, text, context, state.plainText);
|
|
280
|
+
|
|
281
|
+
/** 番号の後ろが見出しでなく本文か。前の行から見出しが付く前の、行そのもので決める。 */
|
|
282
|
+
const carriesBody = (found: NumberedLine | undefined): boolean => found !== undefined && found.heading === "" && found.rest !== "";
|
|
283
|
+
|
|
268
284
|
/** 見出しの無い条のすぐ前の行が見出しだけの行なら、それを条の見出しにする。 */
|
|
269
285
|
const withCaption = (state: State, line: Line, numbered: NumberedLine | undefined): NumberedLine | undefined => {
|
|
270
286
|
const caption = numbered?.kind === "article" && numbered.heading === "" ? state.captions.get(line.number - 1) : undefined;
|
|
@@ -275,12 +291,13 @@ const readLine = (state: State, patterns: StructurePatterns, line: Line, heading
|
|
|
275
291
|
const text = heading === undefined ? line.text : headingLineText(line.text);
|
|
276
292
|
const openNumbers = state.stack.flatMap((frame) => (frame.numbered === undefined ? [] : [frame.numbered]));
|
|
277
293
|
const context = { open: openNumbers, isHeading: heading !== undefined };
|
|
278
|
-
const
|
|
294
|
+
const found = numberedLine(state, patterns, text, context);
|
|
295
|
+
const numbered = withCaption(state, line, found);
|
|
279
296
|
const caption = numbered === undefined && heading === undefined ? captionOf(state.profile, text) : undefined;
|
|
280
297
|
if (caption !== undefined) state.captions.set(line.number, caption);
|
|
281
298
|
// 番号付きの見出しも見出しの通し番号を進める。進めないと、その下の「### 詳細」が前の見出しの番地を名乗る。
|
|
282
299
|
if (numbered !== undefined && heading !== undefined) headingAddress(state, heading.depth);
|
|
283
|
-
if (numbered !== undefined) openNumbered(state, line, numbered, heading?.depth);
|
|
300
|
+
if (numbered !== undefined) openNumbered(state, line, numbered, heading?.depth, carriesBody(found));
|
|
284
301
|
else if (heading !== undefined) openSection(state, line, heading);
|
|
285
302
|
extend(state, line.start + line.text.length);
|
|
286
303
|
// 番号付きの行は番号の後ろだけを読む。「第3条(支払)」の「第3条」を自分への参照として拾わない。
|
|
@@ -309,6 +326,7 @@ export const buildTree = (input: StructureInput, patterns: StructurePatterns): S
|
|
|
309
326
|
widerScopes: new Map(),
|
|
310
327
|
profile: input.profile,
|
|
311
328
|
captions: new Map(),
|
|
329
|
+
plainText: !input.markdown,
|
|
312
330
|
};
|
|
313
331
|
lines.forEach((line) => {
|
|
314
332
|
if (line.text.trim() !== "") readLine(state, patterns, line, headings.get(line.number));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { StructureIssue } from "./issues.ts";
|
|
2
|
+
import { runsOf, type Line } from "./runs.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 日程として並べた日付の順番。箇条書きの続いた項目か、表の続いた行で、日付をちょうど一つ持つものを並びとして読む。
|
|
6
|
+
* 並びの向き(古い順か新しい順か)は多いほうで決め、それに逆らう一歩だけを言う。新しい順に並べた履歴は正しい並び。
|
|
7
|
+
* 向きが決まらない並び(上がりと下がりが同じ数)は何も言わない。逆らう一歩を言えるのは、日付が 4 つ以上の並びだけになる。
|
|
8
|
+
*/
|
|
9
|
+
export type DatedPoint = { readonly offset: number; readonly value: string };
|
|
10
|
+
|
|
11
|
+
/** 並びとして比べられる日付の書き方。年月日、年月、月日は、同じ書き方どうしでしか比べない。 */
|
|
12
|
+
const PRECISIONS = [/^\d{4}-\d{2}-\d{2}$/u, /^\d{4}-\d{2}$/u, /^\d{2}-\d{2}$/u];
|
|
13
|
+
|
|
14
|
+
/** 行ごとの日付。ちょうど一つの行だけを並びに入れる。二つある行(期間)はどちらを並べたのか決められない。 */
|
|
15
|
+
const datedLines = (run: readonly Line[], points: readonly DatedPoint[]): DatedPoint[] =>
|
|
16
|
+
run.flatMap((line) => {
|
|
17
|
+
const inside = points.filter((point) => point.offset >= line.start && point.offset <= line.end);
|
|
18
|
+
return inside.length === 1 && inside[0] !== undefined ? [inside[0]] : [];
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const samePrecision = (dated: readonly DatedPoint[]): boolean => PRECISIONS.some((pattern) => dated.every((point) => pattern.test(point.value)));
|
|
22
|
+
|
|
23
|
+
/** 上がりが多ければ 1、下がりが多ければ -1、同じ数なら 0(向きを決めない)。 */
|
|
24
|
+
const majorityOf = (up: number, down: number): number => Math.sign(up - down);
|
|
25
|
+
|
|
26
|
+
/** 多いほうの向きに逆らう一歩。後ろの項目の日付を指す。 */
|
|
27
|
+
const againstMajority = (dated: readonly DatedPoint[]): StructureIssue[] => {
|
|
28
|
+
const steps = dated
|
|
29
|
+
.slice(1)
|
|
30
|
+
.map((point, index) => ({ point, previous: dated[index], sign: Math.sign(point.value.localeCompare(dated[index]?.value ?? "")) }));
|
|
31
|
+
const up = steps.filter((step) => step.sign > 0).length;
|
|
32
|
+
const down = steps.filter((step) => step.sign < 0).length;
|
|
33
|
+
const majority = majorityOf(up, down);
|
|
34
|
+
if (majority === 0) return [];
|
|
35
|
+
return steps
|
|
36
|
+
.filter((step) => step.sign === -majority)
|
|
37
|
+
.map((step) => ({ offset: step.point.offset, values: { date: step.point.value, previous: step.previous?.value ?? "" } }));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const dateOrderBreaks = (source: string, points: readonly DatedPoint[]): StructureIssue[] =>
|
|
41
|
+
runsOf(source).flatMap((run) => {
|
|
42
|
+
const dated = datedLines(run, points);
|
|
43
|
+
return samePrecision(dated) ? againstMajority(dated) : [];
|
|
44
|
+
});
|