chaffjs 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/detectors/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.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +8 -1
- package/dist/detectors/lexicon.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/plugin.d.ts +11 -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 +3 -1
- package/dist/render/compact.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +17 -5
- package/dist/run.js.map +1 -1
- package/dist/structure/build.d.ts.map +1 -1
- package/dist/structure/build.js +13 -6
- 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/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 +14 -0
- package/dist/structure/total.d.ts.map +1 -0
- package/dist/structure/total.js +119 -0
- package/dist/structure/total.js.map +1 -0
- 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/package.json +3 -3
- package/profiles/statute.yaml +5 -1
- package/rules/date-order.yaml +26 -0
- package/rules/date-weekday-mismatch.yaml +26 -0
- package/rules/internal-jargon.yaml +1 -1
- package/rules/total-mismatch.yaml +26 -0
- package/src/detectors/index.ts +4 -1
- package/src/detectors/lexicon.ts +11 -1
- 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/plugin.ts +15 -2
- package/src/profile/parse.ts +15 -2
- package/src/render/compact.ts +4 -1
- package/src/run.ts +18 -5
- package/src/structure/build.ts +19 -7
- package/src/structure/date-order.ts +44 -0
- package/src/structure/relative-find.ts +13 -0
- package/src/structure/runs.ts +71 -0
- package/src/structure/total.ts +157 -0
- package/src/structure/unnumbered.ts +33 -0
- package/src/structure/weekday.ts +25 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { inDocumentOrder } from "./issues.js";
|
|
2
|
+
// 日付と、その横に書いた曜日の食い違い。暦で決まるので、両方読めれば誤りは機械で言える。
|
|
3
|
+
const FULL_DATE = /^(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})$/u;
|
|
4
|
+
/** 暦の曜日。日曜日が 0。存在しない日(2月30日)は undefined。 */
|
|
5
|
+
export const weekdayOf = (value) => {
|
|
6
|
+
const groups = FULL_DATE.exec(value)?.groups;
|
|
7
|
+
if (groups === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
const [year, month, day] = [Number(groups["y"]), Number(groups["m"]), Number(groups["d"])];
|
|
10
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
11
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day ? date.getUTCDay() : undefined;
|
|
12
|
+
};
|
|
13
|
+
/** 年まで書いた日付で、書いた曜日が暦と違うもの。年の無い日付は曜日を決められないので見ない。 */
|
|
14
|
+
export const weekdayMismatches = (tree) => inDocumentOrder(tree).flatMap((node) => {
|
|
15
|
+
const written = node.attrs["weekday"];
|
|
16
|
+
if (node.kind !== "date" || typeof written !== "number")
|
|
17
|
+
return [];
|
|
18
|
+
const actual = weekdayOf(String(node.attrs["value"]));
|
|
19
|
+
return actual === undefined || actual === written ? [] : [{ offset: node.span.start, values: { date: String(node.attrs["value"]), written, actual } }];
|
|
20
|
+
});
|
|
21
|
+
//# sourceMappingURL=weekday.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"weekday.js","sourceRoot":"","sources":["../../src/structure/weekday.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,8CAA8C;AAE9C,MAAM,SAAS,GAAG,wCAAwC,CAAC;AAE3D,4CAA4C;AAC5C,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAa,EAAsB,EAAE;IAC7D,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAC7C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC3C,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC3F,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACtD,OAAO,IAAI,CAAC,cAAc,EAAE,KAAK,IAAI,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,KAAK,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACxI,CAAC,CAAC;AAEF,oDAAoD;AACpD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,IAAmB,EAAoB,EAAE,CACzE,eAAe,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACtC,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACnE,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACtD,OAAO,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;AACzJ,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chaffjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Prose validation harness. Finds what is hard to read, and never rewrites it for you.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chaff",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@anthropic-ai/sdk": "^0.125.0",
|
|
57
|
-
"@chaffjs/lang-en": "0.
|
|
58
|
-
"@chaffjs/lang-ja": "0.
|
|
57
|
+
"@chaffjs/lang-en": "0.8.0",
|
|
58
|
+
"@chaffjs/lang-ja": "0.9.0",
|
|
59
59
|
"mdast-util-from-markdown": "^2.0.3",
|
|
60
60
|
"mdast-util-frontmatter": "^2.0.1",
|
|
61
61
|
"mdast-util-gfm-table": "^2.0.0",
|
package/profiles/statute.yaml
CHANGED
|
@@ -26,6 +26,9 @@ ja:
|
|
|
26
26
|
address_end: "[^\\p{Script=Han}]|$"
|
|
27
27
|
# 条の見出しは、条の前の行に括弧だけで書く(「(解雇の予告)」)。括弧の中が、すぐ次の条の見出しになる。
|
|
28
28
|
caption: "^[ \\t\u3000]*[((]([^()()]{1,40})[))][ \\t\u3000]*$"
|
|
29
|
+
# 古い法令は第 2 項以降に番号を振らず、全角空白で字下げした行を新しい項(深さ 2)にする。
|
|
30
|
+
# 条の行に本文が続く条(「第三十四条 使用者は、…。」)の中だけ。契約書の「第1条(目的)」の次の字下げの行は、第 1 項の本文。
|
|
31
|
+
unnumbered: { indent: "\u3000", inside: article, depth: 2 }
|
|
29
32
|
# 書いた場所から番地が決まる参照(前条・次項・同号・本条・前各項・前二項・前条第二項)。
|
|
30
33
|
relative:
|
|
31
34
|
before: [前]
|
|
@@ -42,7 +45,8 @@ ja:
|
|
|
42
45
|
# 漢字の後ろの「前条」は語の一部(事前条件)。後ろの決まりは address_end と connectives を使う。
|
|
43
46
|
not_after: "\\p{Script=Han}"
|
|
44
47
|
# 読み替え(「『前条』とあるのは『…』と読み替える」)の括弧の中の前条・同項は、読み替える先の文の言葉。
|
|
45
|
-
|
|
48
|
+
# 中の「第三条」も読み替える先の法令の番地なので、この文書の木では引かず、document の名前の文書を指す。
|
|
49
|
+
substitution: { open: 「, close: 」, after: [とあるのは], before: [とあるのは, とあるのは、], document: (読み替えの中) }
|
|
46
50
|
# 条を書かない「第一項」「第二号」は、書いた条・項の中を指す。
|
|
47
51
|
inside: [項, 号]
|
|
48
52
|
# 並びをつなぐもの(正規表現)。「第三十三条第七項若しくは第九項」の「第九項」は、前の参照(第三十三条)の続き。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
id: date-order
|
|
2
|
+
layer: L3
|
|
3
|
+
status: experimental
|
|
4
|
+
how_to_find: date-order
|
|
5
|
+
requires: [dates]
|
|
6
|
+
severity: warning
|
|
7
|
+
|
|
8
|
+
name:
|
|
9
|
+
ja: 日程の順番が前後している
|
|
10
|
+
en: A date out of order in a schedule
|
|
11
|
+
|
|
12
|
+
why:
|
|
13
|
+
ja: 旅程やマイルストーンのように日付を並べた箇条書きや表で、ひとつだけ前の日付より早い日付があると、書き間違いか順番の入れ替わりかを読み手が確かめることになります。並びが古い順か新しい順かは多いほうで決め、それに逆らう日付だけを指します。
|
|
14
|
+
en: In a list or a table of dates, such as an itinerary or milestones, one date earlier than the one before it makes the reader check whether it is a typo or a swapped row. The direction of the list (oldest or newest first) is taken from most of its steps, and only a date against it is pointed out.
|
|
15
|
+
|
|
16
|
+
message:
|
|
17
|
+
ja: 「{date}」が、前後の日付の並びから外れています(前は「{previous}」)
|
|
18
|
+
en: "{date} breaks the order of the dates around it (after {previous})"
|
|
19
|
+
|
|
20
|
+
how_to_fix:
|
|
21
|
+
ja: 日付を確かめて、書き間違いなら直し、順番が違うなら並べ替えてください。
|
|
22
|
+
en: Check the date. Correct it if it is a typo, or move the row if the order is wrong.
|
|
23
|
+
|
|
24
|
+
levels: { strict: 1, normal: 1, relaxed: 1 }
|
|
25
|
+
|
|
26
|
+
use_for: [business, technical, blog]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
id: date-weekday-mismatch
|
|
2
|
+
layer: L3
|
|
3
|
+
status: experimental
|
|
4
|
+
how_to_find: date-weekday-mismatch
|
|
5
|
+
requires: [dates]
|
|
6
|
+
severity: error
|
|
7
|
+
|
|
8
|
+
name:
|
|
9
|
+
ja: 日付と曜日の食い違い
|
|
10
|
+
en: Date and weekday disagree
|
|
11
|
+
|
|
12
|
+
why:
|
|
13
|
+
ja: 「2026年10月1日(金)」のように、日付と曜日が食い違っていると、読み手はどちらが正しいのか確かめに行くことになります。日程を書き写すときや、年をまたいで予定を使い回すときに起きます。年まで書いた日付の曜日だけを、暦と比べます。
|
|
14
|
+
en: When a date and its weekday disagree ("Friday, 1 October 2026"), the reader has to find out which one is right. It happens when a schedule is copied, or reused for another year. Only dates written with their year are checked against the calendar.
|
|
15
|
+
|
|
16
|
+
message:
|
|
17
|
+
ja: 「{date}」は{actual}です({written}と書いてあります)
|
|
18
|
+
en: "{date} is a {actual}, not a {written}"
|
|
19
|
+
|
|
20
|
+
how_to_fix:
|
|
21
|
+
ja: 日付と曜日のどちらが正しいかを確かめて、片方を直してください。
|
|
22
|
+
en: Check which is right, the date or the weekday, and correct the other.
|
|
23
|
+
|
|
24
|
+
levels: { strict: 1, normal: 1, relaxed: 1 }
|
|
25
|
+
|
|
26
|
+
use_for: [business, technical, blog]
|
|
@@ -15,7 +15,7 @@ why:
|
|
|
15
15
|
|
|
16
16
|
message:
|
|
17
17
|
ja: 「{matched}」は社内でしか通じないかもしれません(そういう語が {count} 箇所)
|
|
18
|
-
en: '"{matched}" may only
|
|
18
|
+
en: '"{matched}" may only be understood inside your team ({count} such words)'
|
|
19
19
|
|
|
20
20
|
how_to_fix:
|
|
21
21
|
ja: 初出のところで普通の言葉に言い換えるか、括弧で説明を添えてください。jargon には辞書形(握る)でも語幹(巻き取)でも書けますが、複合動詞は語幹のほうが確実です。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
id: total-mismatch
|
|
2
|
+
layer: L3
|
|
3
|
+
status: experimental
|
|
4
|
+
how_to_find: total-mismatch
|
|
5
|
+
requires: [quantities]
|
|
6
|
+
severity: error
|
|
7
|
+
|
|
8
|
+
name:
|
|
9
|
+
ja: 合計が内訳の和と合わない
|
|
10
|
+
en: A total that is not the sum of its items
|
|
11
|
+
|
|
12
|
+
why:
|
|
13
|
+
ja: 見積や請求の「合計」が、上に並べた金額の和と合わないと、読み手はどちらが正しいのかを計算し直すことになります。行を足したり消したりしたあとに、合計だけ直し忘れると起きます。箇条書きか表で、合計の語(合計・小計)で始まる行を、同じ単位で同じ列の金額の和と比べます。小計のあとの総計のように、足し方が何通りかあるときは、どの足し方でも合わないときだけ指します。
|
|
14
|
+
en: When the total of a quote or an invoice is not the sum of the amounts listed above it, the reader has to add them up again to find out which is right. It happens when a line is added or removed and the total is not updated. In a list or a table, a line starting with a total word (Total, Subtotal) is compared with the sum of the amounts in the same unit and column above it. When there is more than one way to add them up, such as a grand total after a subtotal and tax, it is pointed out only when none of them matches.
|
|
15
|
+
|
|
16
|
+
message:
|
|
17
|
+
ja: 合計「{written}」が、上の金額の和({sum})と合いません
|
|
18
|
+
en: "The total {written} is not the sum of the amounts above it ({sum})"
|
|
19
|
+
|
|
20
|
+
how_to_fix:
|
|
21
|
+
ja: 内訳の金額と合計のどちらが正しいかを確かめて、片方を直してください。
|
|
22
|
+
en: Check which is right, the items or the total, and correct the other.
|
|
23
|
+
|
|
24
|
+
levels: { strict: 1, normal: 1, relaxed: 1 }
|
|
25
|
+
|
|
26
|
+
use_for: [business, technical, blog]
|
package/src/detectors/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { concreteEvidence, dashDensity, emojiDensity, ngramRepetition, undefined
|
|
|
15
15
|
import { aiTell, contractionMix, cushionDensity, hedgingDensity, repeatedConjunction, unqualifiedSuperlative } from "./lexicon.ts";
|
|
16
16
|
import { internalJargon, properNounDensity, requiredSections } from "./team.ts";
|
|
17
17
|
import { latinSpacing, preferredTerm } from "./orthography.ts";
|
|
18
|
-
import { danglingReference, duplicateDefinition, numberingGap } from "./structure-tree.ts";
|
|
18
|
+
import { danglingReference, dateOrder, dateWeekdayMismatch, duplicateDefinition, numberingGap, totalMismatch } from "./structure-tree.ts";
|
|
19
19
|
|
|
20
20
|
/** rule 定義の how_to_find がここを引く。rule 側は実装を知らない。 */
|
|
21
21
|
export const DETECTORS: Readonly<Record<string, Detector>> = {
|
|
@@ -58,6 +58,9 @@ export const DETECTORS: Readonly<Record<string, Detector>> = {
|
|
|
58
58
|
"preferred-term": preferredTerm,
|
|
59
59
|
"latin-spacing": latinSpacing,
|
|
60
60
|
"dangling-reference": danglingReference,
|
|
61
|
+
"date-weekday-mismatch": dateWeekdayMismatch,
|
|
62
|
+
"date-order": dateOrder,
|
|
63
|
+
"total-mismatch": totalMismatch,
|
|
61
64
|
"numbering-gap": numberingGap,
|
|
62
65
|
"duplicate-definition": duplicateDefinition,
|
|
63
66
|
};
|
package/src/detectors/lexicon.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { joinWords } from "./word-list.ts";
|
|
1
2
|
import { proseText } from "../measure.ts";
|
|
2
3
|
import { wordsOf } from "./structure.ts";
|
|
3
4
|
import type { Detector, Finding, ProseDocument, Sentence } from "../plugin.ts";
|
|
@@ -118,7 +119,16 @@ export const aiTell: Detector = (doc, options): Finding[] => {
|
|
|
118
119
|
line: 0,
|
|
119
120
|
column: 0,
|
|
120
121
|
quote: first.text.trim(),
|
|
121
|
-
values: {
|
|
122
|
+
values: {
|
|
123
|
+
word: joinWords(
|
|
124
|
+
found.map(([pattern]) => pattern),
|
|
125
|
+
doc.language,
|
|
126
|
+
),
|
|
127
|
+
count: found.length,
|
|
128
|
+
density: score,
|
|
129
|
+
limit: options.limit,
|
|
130
|
+
offset: first.span.start,
|
|
131
|
+
},
|
|
122
132
|
},
|
|
123
133
|
];
|
|
124
134
|
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Detector, Finding, ProseDocument } from "../plugin.ts";
|
|
2
|
-
import { danglingReferences, duplicateDefinitions, type StructureIssue } from "../structure/issues.ts";
|
|
2
|
+
import { danglingReferences, duplicateDefinitions, inDocumentOrder, type StructureIssue } from "../structure/issues.ts";
|
|
3
3
|
import { numberingBreaks } from "../structure/numbering.ts";
|
|
4
|
+
import { weekdayMismatches } from "../structure/weekday.ts";
|
|
5
|
+
import { dateOrderBreaks } from "../structure/date-order.ts";
|
|
6
|
+
import { totalMismatches, type Amount } from "../structure/total.ts";
|
|
4
7
|
|
|
5
8
|
const QUOTE_LENGTH = 80;
|
|
6
9
|
|
|
@@ -30,3 +33,66 @@ const findingsOf =
|
|
|
30
33
|
export const danglingReference: Detector = findingsOf("dangling-reference", danglingReferences);
|
|
31
34
|
export const numberingGap: Detector = findingsOf("numbering-gap", numberingBreaks);
|
|
32
35
|
export const duplicateDefinition: Detector = findingsOf("duplicate-definition", duplicateDefinitions);
|
|
36
|
+
|
|
37
|
+
/** 曜日の名前は言語パッケージの語彙表(weekday、日曜日から順)から取る。無ければ番号のまま。 */
|
|
38
|
+
const dayName = (doc: ProseDocument, day: unknown): string => {
|
|
39
|
+
const index = Number(day);
|
|
40
|
+
return doc.lexicons["weekday"]?.[index]?.pattern ?? String(index);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const dateWeekdayMismatch: Detector = (doc): Finding[] =>
|
|
44
|
+
doc.structure === undefined
|
|
45
|
+
? []
|
|
46
|
+
: weekdayMismatches(doc.structure).map((issue) => ({
|
|
47
|
+
rule: "date-weekday-mismatch",
|
|
48
|
+
severity: "error",
|
|
49
|
+
line: 0,
|
|
50
|
+
column: 0,
|
|
51
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
52
|
+
values: {
|
|
53
|
+
date: String(issue.values["date"]),
|
|
54
|
+
written: dayName(doc, issue.values["written"]),
|
|
55
|
+
actual: dayName(doc, issue.values["actual"]),
|
|
56
|
+
offset: issue.offset,
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
/** 木の日付を、原文の位置と一緒に並べる。 */
|
|
61
|
+
const datedPoints = (tree: NonNullable<ProseDocument["structure"]>): { offset: number; value: string }[] =>
|
|
62
|
+
inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ offset: node.span.start, value: String(node.attrs["value"]) }] : []));
|
|
63
|
+
|
|
64
|
+
/** 日程の並びに逆らう日付。並びの読み方は原文の行を見るので、木と原文の両方を渡す。 */
|
|
65
|
+
export const dateOrder: Detector = (doc): Finding[] =>
|
|
66
|
+
doc.structure === undefined
|
|
67
|
+
? []
|
|
68
|
+
: dateOrderBreaks(doc.source, datedPoints(doc.structure)).map((issue) => ({
|
|
69
|
+
rule: "date-order",
|
|
70
|
+
severity: "warning",
|
|
71
|
+
line: 0,
|
|
72
|
+
column: 0,
|
|
73
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
74
|
+
values: { ...issue.values, offset: issue.offset },
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
/** 木の数量を、原文の位置と一緒に並べる。 */
|
|
78
|
+
const amountsOf = (tree: NonNullable<ProseDocument["structure"]>): Amount[] =>
|
|
79
|
+
inDocumentOrder(tree).flatMap((node) =>
|
|
80
|
+
node.kind === "quantity" ? [{ offset: node.span.start, end: node.span.end, value: Number(node.attrs["value"]), unit: String(node.attrs["unit"]) }] : [],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
/** 合計の行が、上の金額の和と合わない。合計の語は言語パッケージの語彙表(total-label)から取る。 */
|
|
84
|
+
export const totalMismatch: Detector = (doc): Finding[] =>
|
|
85
|
+
doc.structure === undefined
|
|
86
|
+
? []
|
|
87
|
+
: totalMismatches(
|
|
88
|
+
doc.source,
|
|
89
|
+
amountsOf(doc.structure),
|
|
90
|
+
(doc.lexicons["total-label"] ?? []).map((entry) => entry.pattern),
|
|
91
|
+
).map((issue) => ({
|
|
92
|
+
rule: "total-mismatch",
|
|
93
|
+
severity: "error",
|
|
94
|
+
line: 0,
|
|
95
|
+
column: 0,
|
|
96
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
97
|
+
values: { ...issue.values, offset: issue.offset },
|
|
98
|
+
}));
|
package/src/detectors/team.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { proseText } from "../measure.ts";
|
|
2
|
+
import { joinWords } from "./word-list.ts";
|
|
2
3
|
import { wordsOf } from "./structure.ts";
|
|
3
4
|
import type { Detector, Finding, Sentence, Token } from "../plugin.ts";
|
|
4
5
|
|
|
@@ -12,7 +13,11 @@ import type { Detector, Finding, Sentence, Token } from "../plugin.ts";
|
|
|
12
13
|
* 表層でも原形でも当てる。利用者は辞書形で書く(「握る」)が、本文は活用している(「握った」)。
|
|
13
14
|
* 品詞解析があれば原形で当たり、無くても表層で当たる。要求はしない。
|
|
14
15
|
*/
|
|
15
|
-
const matches = (sentence: Sentence, word: string): boolean =>
|
|
16
|
+
const matches = (sentence: Sentence, word: string): boolean => {
|
|
17
|
+
// 大文字小文字は区別しない。英語の文頭の「Circle back」も「circle back」として当てる。
|
|
18
|
+
const wanted = word.toLowerCase();
|
|
19
|
+
return sentence.text.toLowerCase().includes(wanted) || (sentence.tokens ?? []).some((token) => token.lemma?.toLowerCase() === wanted);
|
|
20
|
+
};
|
|
16
21
|
|
|
17
22
|
export const internalJargon: Detector = (doc, options): Finding[] => {
|
|
18
23
|
const words = (options.lexicon ?? []).map((entry) => entry.pattern);
|
|
@@ -53,7 +58,7 @@ export const requiredSections: Detector = (doc, options): Finding[] => {
|
|
|
53
58
|
line: 0,
|
|
54
59
|
column: 0,
|
|
55
60
|
quote: headings.filter((heading) => heading.length > 0).join(" / "),
|
|
56
|
-
values: { word: missing.
|
|
61
|
+
values: { word: joinWords(missing, doc.language), count: missing.length, limit: options.limit, offset: 0 },
|
|
57
62
|
},
|
|
58
63
|
];
|
|
59
64
|
};
|
package/src/plugin.ts
CHANGED
|
@@ -213,6 +213,11 @@ export type DocumentProfile = {
|
|
|
213
213
|
readonly caption?: string | undefined;
|
|
214
214
|
/** 前条・同項のように、書いた場所から番地が決まる参照の読み方。無ければ読まない。 */
|
|
215
215
|
readonly relative?: RelativeVocabulary | undefined;
|
|
216
|
+
/**
|
|
217
|
+
* 番号を書かない単位(古い法令の第 2 項以降)。indent で始まる行を、inside の種類の単位の中の、depth の深さの次の単位にする。
|
|
218
|
+
* inside の単位の行に、番号の後ろの本文が続くときだけ。
|
|
219
|
+
*/
|
|
220
|
+
readonly unnumbered?: { readonly indent: string; readonly inside: string; readonly depth: number } | undefined;
|
|
216
221
|
};
|
|
217
222
|
|
|
218
223
|
/**
|
|
@@ -239,9 +244,17 @@ export type RelativeVocabulary = {
|
|
|
239
244
|
readonly notAfter: string | undefined;
|
|
240
245
|
/**
|
|
241
246
|
* 読み替え(「『前条』とあるのは『…』」)の括弧。中の前条・同項は読み替える先の文の言葉で、ここからは決められないので読まない。
|
|
242
|
-
* 閉じの直後に after、開きの直前に before
|
|
247
|
+
* 閉じの直後に after、開きの直前に before のどれかがあるものだけ。中の番地を名指しした参照は、document の名前の文書を指す。
|
|
243
248
|
*/
|
|
244
|
-
readonly substitution:
|
|
249
|
+
readonly substitution:
|
|
250
|
+
| {
|
|
251
|
+
readonly open: string;
|
|
252
|
+
readonly close: string;
|
|
253
|
+
readonly after: readonly string[];
|
|
254
|
+
readonly before: readonly string[];
|
|
255
|
+
readonly document: string | undefined;
|
|
256
|
+
}
|
|
257
|
+
| undefined;
|
|
245
258
|
/** 条を書かずにこの単位で始まる番地(法令の「第一項」「第二号」)は、書いた場所を含むまとまりの中を指す。 */
|
|
246
259
|
readonly inside: readonly string[];
|
|
247
260
|
/**
|
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
|
@@ -2,6 +2,9 @@ import type { RuleDefinition } from "../plugin.ts";
|
|
|
2
2
|
import type { RunResult } from "../run.ts";
|
|
3
3
|
import { messageOf } from "./text.ts";
|
|
4
4
|
|
|
5
|
+
/** 「1 finding」「2 findings」。 */
|
|
6
|
+
const counted = (count: number, noun: string): string => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
7
|
+
|
|
5
8
|
/** エンジニア向け。1 件 2 行。 */
|
|
6
9
|
export const renderCompact = (header: string, result: RunResult, rules: readonly RuleDefinition[], language: string): string => {
|
|
7
10
|
const byId = new Map(rules.map((rule) => [rule.id, rule]));
|
|
@@ -11,7 +14,7 @@ export const renderCompact = (header: string, result: RunResult, rules: readonly
|
|
|
11
14
|
const at = `${finding.line}:${finding.column}`.padEnd(8);
|
|
12
15
|
return [` ${at}${finding.severity.padEnd(8)}${messageOf(rule, finding, language)}`, ` ${" ".repeat(8)}${finding.rule}`];
|
|
13
16
|
});
|
|
14
|
-
const tail = [
|
|
17
|
+
const tail = [counted(result.findings.length, "finding"), result.skipped.length > 0 ? `${counted(result.skipped.length, "rule")} not run` : undefined].filter(
|
|
15
18
|
(part) => part !== undefined,
|
|
16
19
|
);
|
|
17
20
|
return ["", header, "", ...lines, "", tail.join(", "), ""].join("\n");
|
package/src/run.ts
CHANGED
|
@@ -3,6 +3,7 @@ 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";
|
|
8
9
|
import { uiLanguageOf } from "./ui.ts";
|
|
@@ -32,6 +33,18 @@ const reasonsFor = (doc: ProseDocument): Reasons => REASONS[uiLanguageOf(doc.lan
|
|
|
32
33
|
* 構造の rule が動けない理由。木を作れない言語か、番号の行を読めなかった文書。
|
|
33
34
|
* 読めなかった文書で「参照先が無い」が 0 件なのは、確かめた結果ではないので、読めなかったと言う。
|
|
34
35
|
*/
|
|
36
|
+
/**
|
|
37
|
+
* 木を読む rule の要求。structure は番号の並びを読む(読めなかった文書では動かない)。dates と quantities は木の日付と数量だけを
|
|
38
|
+
* 読むので、番号を読めなかった文書でも動く。どちらも capability ではなく、adapter が木を作れるかで決まる。
|
|
39
|
+
*/
|
|
40
|
+
const TREE_NEEDS: ReadonlySet<string> = new Set(["structure", "dates", "quantities"]);
|
|
41
|
+
const VALUE_NEEDS: readonly string[] = ["dates", "quantities"];
|
|
42
|
+
|
|
43
|
+
const treeNeed = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
44
|
+
if (rule.requires.includes("structure")) return treeProblem(doc);
|
|
45
|
+
return rule.requires.some((need) => VALUE_NEEDS.includes(need)) && doc.structure === undefined ? reasonsFor(doc).noStructure(doc.language) : undefined;
|
|
46
|
+
};
|
|
47
|
+
|
|
35
48
|
const treeProblem = (doc: ProseDocument): string | undefined => {
|
|
36
49
|
if (doc.structure === undefined) return reasonsFor(doc).noStructure(doc.language);
|
|
37
50
|
const unread = unreadOf(doc);
|
|
@@ -68,7 +81,7 @@ const hasTokens = (doc: ProseDocument): boolean => doc.sentences.length === 0 ||
|
|
|
68
81
|
/** 要求を満たさない rule は動かせない。満たさないまま動かすと「指摘 0 件」が保証に見える。 */
|
|
69
82
|
const unmet = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
70
83
|
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return reasonsFor(doc).otherLanguage(doc.language);
|
|
71
|
-
const missing = rule.requires.filter((need) => need
|
|
84
|
+
const missing = rule.requires.filter((need) => !TREE_NEEDS.has(need)).find((need) => !has(doc.capabilities, need));
|
|
72
85
|
if (missing !== undefined) return reasonsFor(doc).noCapability(missing);
|
|
73
86
|
return undefined;
|
|
74
87
|
};
|
|
@@ -107,7 +120,7 @@ const place = (starts: readonly number[], finding: Finding): Finding => {
|
|
|
107
120
|
* 元の指摘は消さない。`padded-intro` のように単独でも正しい指摘が混ざっており、
|
|
108
121
|
* まとめるために消すと本物の指摘が見えなくなる。
|
|
109
122
|
*/
|
|
110
|
-
const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: number, starts: readonly number[]): Finding[] => {
|
|
123
|
+
const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: number, starts: readonly number[], language: string): Finding[] => {
|
|
111
124
|
const fired = rule.from.filter((id) => found.some((finding) => finding.rule === id));
|
|
112
125
|
if (fired.length < limit) return [];
|
|
113
126
|
const first = found.find((finding) => fired.includes(finding.rule));
|
|
@@ -118,7 +131,7 @@ const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: num
|
|
|
118
131
|
line: 0,
|
|
119
132
|
column: 0,
|
|
120
133
|
quote: first?.quote ?? "",
|
|
121
|
-
values: { word: fired
|
|
134
|
+
values: { word: joinWords(fired, language), count: fired.length, limit, offset: first === undefined ? 0 : Number(first.values["offset"] ?? 0) },
|
|
122
135
|
}),
|
|
123
136
|
];
|
|
124
137
|
};
|
|
@@ -158,7 +171,7 @@ export const runRules = (
|
|
|
158
171
|
if (noTags !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTags }] };
|
|
159
172
|
// 木は capability ではなく、adapter が structure を持つかで決まる。持たない言語で動かすと「参照先が無い」が 0 件に見える。
|
|
160
173
|
// 段階を見た後で聞く。doc.structure は触れたときに木を作るので、止めている rule のために作らない。
|
|
161
|
-
const noTree = rule
|
|
174
|
+
const noTree = treeNeed(rule, doc);
|
|
162
175
|
if (noTree !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTree }] };
|
|
163
176
|
// 複合シグナルは二段目で扱う。一段目では「検出器が無い」と言わせない。
|
|
164
177
|
if (rule.from.length > 0) return acc;
|
|
@@ -181,7 +194,7 @@ export const runRules = (
|
|
|
181
194
|
);
|
|
182
195
|
const composites = applicable
|
|
183
196
|
.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));
|
|
197
|
+
.flatMap((rule) => compositeOf(rule, outcome.findings, limitFor(rule, levelFor(rule, settings, experimental), genre, limits), starts, doc.language));
|
|
185
198
|
const all = [...outcome.findings, ...composites];
|
|
186
199
|
return { ...outcome, findings: [...all].sort((left, right) => left.line - right.line), forcedExperimental: forced };
|
|
187
200
|
};
|
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 にする。番号のまとまりは必ず見出しの内側に入るので、
|
|
@@ -96,7 +97,7 @@ const headingAddress = (state: State, depth: number): string => {
|
|
|
96
97
|
* 見出しに書いた番号(「## 第3条」)は、見出しの深さで入れ子にする。本文の番号は見出しの内側に入る。
|
|
97
98
|
* 見出しの深さを捨てると、「## 第3条」の下の「### 詳細」が条の外に出てしまう。
|
|
98
99
|
*/
|
|
99
|
-
const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingDepth: number | undefined): void => {
|
|
100
|
+
const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingDepth: number | undefined, bodyOnLine: boolean): void => {
|
|
100
101
|
const attrs = { label: numbered.label, ...(numbered.heading === "" ? {} : { heading: numbered.heading }) };
|
|
101
102
|
const rank = headingDepth ?? NUMBERED_RANK + numbered.depth;
|
|
102
103
|
// 番地は、自分と同じか内側を閉じてから決める。閉じる前だと、同じ深さの前の条を親と取り違える。
|
|
@@ -108,7 +109,7 @@ const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingD
|
|
|
108
109
|
ordinalTo: numbered.ordinalTo,
|
|
109
110
|
numbering: numbered.numbering,
|
|
110
111
|
};
|
|
111
|
-
open(state, { draft, rank, numbered });
|
|
112
|
+
open(state, { draft, rank, numbered, bodyOnLine });
|
|
112
113
|
};
|
|
113
114
|
|
|
114
115
|
const openSection = (state: State, line: Line, heading: Heading): void => {
|
|
@@ -130,7 +131,7 @@ type LeafFinder = (patterns: StructurePatterns, text: string, profile: DocumentP
|
|
|
130
131
|
|
|
131
132
|
const LEAVES: readonly { readonly kind: StructureKind; readonly find: LeafFinder }[] = [
|
|
132
133
|
{ kind: "definition", find: (patterns, text) => patterns.definitions(text) },
|
|
133
|
-
{ kind: "reference", find: (patterns, text) => patterns.references(text) },
|
|
134
|
+
{ kind: "reference", find: (patterns, text, profile) => substitutedDocuments(patterns.references(text), text, profile) },
|
|
134
135
|
{
|
|
135
136
|
kind: "reference",
|
|
136
137
|
find: (patterns, text, profile) => {
|
|
@@ -265,6 +266,16 @@ const captionOf = (profile: DocumentProfile | undefined, text: string): string |
|
|
|
265
266
|
return words === "" ? undefined : words;
|
|
266
267
|
};
|
|
267
268
|
|
|
269
|
+
const openUnits = (state: State): OpenUnit[] =>
|
|
270
|
+
state.stack.flatMap((frame) => (frame.numbered === undefined ? [] : [{ numbered: frame.numbered, bodyOnLine: frame.bodyOnLine === true }]));
|
|
271
|
+
|
|
272
|
+
/** 言語の番号の読み方、文書の種類の番号を書かない単位、言語を問わない通し番号の順に読む。 */
|
|
273
|
+
const numberedLine = (state: State, patterns: StructurePatterns, text: string, context: NumberingContext): NumberedLine | undefined =>
|
|
274
|
+
patterns.numbered(text, context) ?? unnumberedUnit(text, openUnits(state), state.profile?.unnumbered) ?? universalNumber(patterns, text, context);
|
|
275
|
+
|
|
276
|
+
/** 番号の後ろが見出しでなく本文か。前の行から見出しが付く前の、行そのもので決める。 */
|
|
277
|
+
const carriesBody = (found: NumberedLine | undefined): boolean => found !== undefined && found.heading === "" && found.rest !== "";
|
|
278
|
+
|
|
268
279
|
/** 見出しの無い条のすぐ前の行が見出しだけの行なら、それを条の見出しにする。 */
|
|
269
280
|
const withCaption = (state: State, line: Line, numbered: NumberedLine | undefined): NumberedLine | undefined => {
|
|
270
281
|
const caption = numbered?.kind === "article" && numbered.heading === "" ? state.captions.get(line.number - 1) : undefined;
|
|
@@ -275,12 +286,13 @@ const readLine = (state: State, patterns: StructurePatterns, line: Line, heading
|
|
|
275
286
|
const text = heading === undefined ? line.text : headingLineText(line.text);
|
|
276
287
|
const openNumbers = state.stack.flatMap((frame) => (frame.numbered === undefined ? [] : [frame.numbered]));
|
|
277
288
|
const context = { open: openNumbers, isHeading: heading !== undefined };
|
|
278
|
-
const
|
|
289
|
+
const found = numberedLine(state, patterns, text, context);
|
|
290
|
+
const numbered = withCaption(state, line, found);
|
|
279
291
|
const caption = numbered === undefined && heading === undefined ? captionOf(state.profile, text) : undefined;
|
|
280
292
|
if (caption !== undefined) state.captions.set(line.number, caption);
|
|
281
293
|
// 番号付きの見出しも見出しの通し番号を進める。進めないと、その下の「### 詳細」が前の見出しの番地を名乗る。
|
|
282
294
|
if (numbered !== undefined && heading !== undefined) headingAddress(state, heading.depth);
|
|
283
|
-
if (numbered !== undefined) openNumbered(state, line, numbered, heading?.depth);
|
|
295
|
+
if (numbered !== undefined) openNumbered(state, line, numbered, heading?.depth, carriesBody(found));
|
|
284
296
|
else if (heading !== undefined) openSection(state, line, heading);
|
|
285
297
|
extend(state, line.start + line.text.length);
|
|
286
298
|
// 番号付きの行は番号の後ろだけを読む。「第3条(支払)」の「第3条」を自分への参照として拾わない。
|
|
@@ -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
|
+
});
|
|
@@ -105,6 +105,19 @@ const substitutionSpans = (text: string, quote: Substitution | undefined): Span[
|
|
|
105
105
|
(span) => quote.after.some((marker) => text.startsWith(marker, span.end)) || quote.before.some((marker) => text.slice(0, span.start).endsWith(marker)),
|
|
106
106
|
);
|
|
107
107
|
|
|
108
|
+
/** 読み替えの括弧の中の、文書を名指ししていない参照に、読み替える先の文書の名前を付ける。 */
|
|
109
|
+
export const substitutedDocuments = (mentions: readonly Mention[], text: string, profile: DocumentProfile | undefined): readonly Mention[] => {
|
|
110
|
+
const quote = profile?.relative?.substitution;
|
|
111
|
+
const document = quote?.document;
|
|
112
|
+
if (document === undefined) return mentions;
|
|
113
|
+
const spans = substitutionSpans(text, quote);
|
|
114
|
+
return mentions.map((mention) =>
|
|
115
|
+
mention.attrs["document"] === undefined && spans.some((span) => span.start <= mention.start && mention.start < span.end)
|
|
116
|
+
? { ...mention, attrs: { ...mention.attrs, document } }
|
|
117
|
+
: mention,
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
108
121
|
/** 閉じた括弧書きを内側から外す。 */
|
|
109
122
|
const withoutAsides = (gap: string, aside: RelativeVocabulary["aside"]): string => {
|
|
110
123
|
if (aside === undefined) return gap;
|