chaffjs 0.23.0 → 0.25.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/bare-comparative.d.ts +26 -0
- package/dist/detectors/bare-comparative.d.ts.map +1 -0
- package/dist/detectors/bare-comparative.js +117 -0
- package/dist/detectors/bare-comparative.js.map +1 -0
- package/dist/detectors/chat-citation.d.ts +6 -0
- package/dist/detectors/chat-citation.d.ts.map +1 -1
- package/dist/detectors/chat-citation.js +26 -1
- package/dist/detectors/chat-citation.js.map +1 -1
- package/dist/detectors/clause-shape.d.ts.map +1 -1
- package/dist/detectors/clause-shape.js +19 -5
- package/dist/detectors/clause-shape.js.map +1 -1
- package/dist/detectors/connective-hierarchy.d.ts +16 -0
- package/dist/detectors/connective-hierarchy.d.ts.map +1 -0
- package/dist/detectors/connective-hierarchy.js +68 -0
- package/dist/detectors/connective-hierarchy.js.map +1 -0
- package/dist/detectors/definition-use.d.ts.map +1 -1
- package/dist/detectors/definition-use.js +4 -2
- package/dist/detectors/definition-use.js.map +1 -1
- package/dist/detectors/generic-heading.d.ts +8 -0
- package/dist/detectors/generic-heading.d.ts.map +1 -0
- package/dist/detectors/generic-heading.js +26 -0
- package/dist/detectors/generic-heading.js.map +1 -0
- package/dist/detectors/ime-residue.d.ts +13 -0
- package/dist/detectors/ime-residue.d.ts.map +1 -0
- package/dist/detectors/ime-residue.js +27 -0
- package/dist/detectors/ime-residue.js.map +1 -0
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +9 -2
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/lookalike-character.d.ts +18 -0
- package/dist/detectors/lookalike-character.d.ts.map +1 -0
- package/dist/detectors/lookalike-character.js +50 -0
- package/dist/detectors/lookalike-character.js.map +1 -0
- package/dist/detectors/lowercase-name.d.ts +9 -0
- package/dist/detectors/lowercase-name.d.ts.map +1 -0
- package/dist/detectors/lowercase-name.js +40 -0
- package/dist/detectors/lowercase-name.js.map +1 -0
- package/dist/detectors/missing-final-period.d.ts +17 -0
- package/dist/detectors/missing-final-period.d.ts.map +1 -0
- package/dist/detectors/missing-final-period.js +62 -0
- package/dist/detectors/missing-final-period.js.map +1 -0
- package/dist/detectors/missing-space-after-comma.d.ts +10 -0
- package/dist/detectors/missing-space-after-comma.d.ts.map +1 -0
- package/dist/detectors/missing-space-after-comma.js +42 -0
- package/dist/detectors/missing-space-after-comma.js.map +1 -0
- package/dist/detectors/registry/bare-comparative.d.ts +3 -0
- package/dist/detectors/registry/bare-comparative.d.ts.map +1 -0
- package/dist/detectors/registry/bare-comparative.js +3 -0
- package/dist/detectors/registry/bare-comparative.js.map +1 -0
- package/dist/detectors/registry/calendar-name-case.d.ts +3 -0
- package/dist/detectors/registry/calendar-name-case.d.ts.map +1 -0
- package/dist/detectors/registry/calendar-name-case.js +3 -0
- package/dist/detectors/registry/calendar-name-case.js.map +1 -0
- package/dist/detectors/registry/connective-hierarchy.d.ts +3 -0
- package/dist/detectors/registry/connective-hierarchy.d.ts.map +1 -0
- package/dist/detectors/registry/connective-hierarchy.js +3 -0
- package/dist/detectors/registry/connective-hierarchy.js.map +1 -0
- package/dist/detectors/registry/generic-heading.d.ts +3 -0
- package/dist/detectors/registry/generic-heading.d.ts.map +1 -0
- package/dist/detectors/registry/generic-heading.js +3 -0
- package/dist/detectors/registry/generic-heading.js.map +1 -0
- package/dist/detectors/registry/ime-residue.d.ts +3 -0
- package/dist/detectors/registry/ime-residue.d.ts.map +1 -0
- package/dist/detectors/registry/ime-residue.js +3 -0
- package/dist/detectors/registry/ime-residue.js.map +1 -0
- package/dist/detectors/registry/lookalike-character.d.ts +3 -0
- package/dist/detectors/registry/lookalike-character.d.ts.map +1 -0
- package/dist/detectors/registry/lookalike-character.js +3 -0
- package/dist/detectors/registry/lookalike-character.js.map +1 -0
- package/dist/detectors/registry/missing-final-period.d.ts +3 -0
- package/dist/detectors/registry/missing-final-period.d.ts.map +1 -0
- package/dist/detectors/registry/missing-final-period.js +3 -0
- package/dist/detectors/registry/missing-final-period.js.map +1 -0
- package/dist/detectors/registry/missing-space-after-comma.d.ts +3 -0
- package/dist/detectors/registry/missing-space-after-comma.d.ts.map +1 -0
- package/dist/detectors/registry/missing-space-after-comma.js +3 -0
- package/dist/detectors/registry/missing-space-after-comma.js.map +1 -0
- package/dist/detectors/registry/unit-spacing-consistency.d.ts +3 -0
- package/dist/detectors/registry/unit-spacing-consistency.d.ts.map +1 -0
- package/dist/detectors/registry/unit-spacing-consistency.js +3 -0
- package/dist/detectors/registry/unit-spacing-consistency.js.map +1 -0
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +8 -4
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +32 -7
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/technical-docs.d.ts.map +1 -1
- package/dist/detectors/technical-docs.js +14 -0
- package/dist/detectors/technical-docs.js.map +1 -1
- package/dist/detectors/unit-spacing.d.ts +25 -0
- package/dist/detectors/unit-spacing.d.ts.map +1 -0
- package/dist/detectors/unit-spacing.js +50 -0
- package/dist/detectors/unit-spacing.js.map +1 -0
- package/dist/quoted-span.d.ts +7 -0
- package/dist/quoted-span.d.ts.map +1 -1
- package/dist/quoted-span.js +20 -0
- package/dist/quoted-span.js.map +1 -1
- package/dist/structure/definition-use.d.ts +14 -4
- package/dist/structure/definition-use.d.ts.map +1 -1
- package/dist/structure/definition-use.js +29 -9
- package/dist/structure/definition-use.js.map +1 -1
- package/package.json +3 -3
- package/rules/calendar-name-case.yaml +55 -0
- package/rules/comparative-without-baseline.yaml +71 -0
- package/rules/connective-hierarchy.yaml +62 -0
- package/rules/cross-doc-broken-link.yaml +1 -1
- package/rules/cross-doc-duplicate-definition.yaml +1 -1
- package/rules/cross-doc-fact-conflict.yaml +1 -1
- package/rules/cross-doc-term-variant.yaml +5 -3
- package/rules/ime-residue.yaml +55 -0
- package/rules/lookalike-character.yaml +82 -0
- package/rules/missing-final-period.yaml +76 -0
- package/rules/missing-space-after-comma.yaml +57 -0
- package/rules/numbered-generic-heading.yaml +111 -0
- package/rules/undefined-acronym.yaml +1 -0
- package/rules/unit-spacing-consistency.yaml +74 -0
- package/skills/chaff/SKILL.md +20 -3
- package/src/detectors/bare-comparative.ts +152 -0
- package/src/detectors/chat-citation.ts +28 -1
- package/src/detectors/clause-shape.ts +20 -6
- package/src/detectors/connective-hierarchy.ts +99 -0
- package/src/detectors/definition-use.ts +13 -3
- package/src/detectors/generic-heading.ts +30 -0
- package/src/detectors/ime-residue.ts +37 -0
- package/src/detectors/lexicon.ts +11 -2
- package/src/detectors/lookalike-character.ts +75 -0
- package/src/detectors/lowercase-name.ts +58 -0
- package/src/detectors/missing-final-period.ts +79 -0
- package/src/detectors/missing-space-after-comma.ts +54 -0
- package/src/detectors/registry/bare-comparative.ts +4 -0
- package/src/detectors/registry/calendar-name-case.ts +4 -0
- package/src/detectors/registry/connective-hierarchy.ts +4 -0
- package/src/detectors/registry/generic-heading.ts +4 -0
- package/src/detectors/registry/ime-residue.ts +4 -0
- package/src/detectors/registry/lookalike-character.ts +4 -0
- package/src/detectors/registry/missing-final-period.ts +4 -0
- package/src/detectors/registry/missing-space-after-comma.ts +4 -0
- package/src/detectors/registry/unit-spacing-consistency.ts +4 -0
- package/src/detectors/signals.ts +9 -5
- package/src/detectors/structure-tree.ts +55 -30
- package/src/detectors/technical-docs.ts +13 -0
- package/src/detectors/unit-spacing.ts +71 -0
- package/src/quoted-span.ts +25 -0
- package/src/structure/definition-use.ts +42 -10
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { Detector, Finding, ProseDocument } from "../plugin.ts";
|
|
2
|
-
import { definedTerms, unusedDefinitions, usesBeforeDefinition, type BodyText } from "../structure/definition-use.ts";
|
|
2
|
+
import { definedTerms, unusedDefinitions, usesBeforeDefinition, type BodyText, type Mentioned } from "../structure/definition-use.ts";
|
|
3
3
|
import { expansionConflicts, expansionsIn } from "../acronym-expansions.ts";
|
|
4
4
|
import { linesOf, lineNumberAt } from "../structure/lines.ts";
|
|
5
|
+
import { isQuotedAlone } from "../quoted-span.ts";
|
|
5
6
|
import { quoteAt } from "./structure-tree.ts";
|
|
6
7
|
|
|
7
8
|
const bodyOf = (doc: ProseDocument): BodyText[] => doc.sentences.map((sentence) => ({ start: sentence.span.start, text: sentence.text }));
|
|
8
9
|
|
|
10
|
+
const mentionedIn =
|
|
11
|
+
(doc: ProseDocument): Mentioned =>
|
|
12
|
+
(start, end) =>
|
|
13
|
+
isQuotedAlone(doc.source, { start, end });
|
|
14
|
+
|
|
9
15
|
const findingAt = (doc: ProseDocument, offset: number, values: Readonly<Record<string, string | number>>): Finding => ({
|
|
10
16
|
rule: "",
|
|
11
17
|
severity: "warning",
|
|
@@ -19,13 +25,17 @@ const findingAt = (doc: ProseDocument, offset: number, values: Readonly<Record<s
|
|
|
19
25
|
export const unusedDefinition: Detector = (doc): Finding[] =>
|
|
20
26
|
doc.structure === undefined
|
|
21
27
|
? []
|
|
22
|
-
: unusedDefinitions(definedTerms(doc.structure), bodyOf(doc)).map((defined) =>
|
|
28
|
+
: unusedDefinitions(definedTerms(doc.structure), bodyOf(doc), mentionedIn(doc)).map((defined) =>
|
|
29
|
+
findingAt(doc, defined.span.start, { term: defined.term }),
|
|
30
|
+
);
|
|
23
31
|
|
|
24
32
|
/** 文の途中で括弧に入れて定義した語を、定義より前で使っている所。 */
|
|
25
33
|
export const useBeforeDefinition: Detector = (doc): Finding[] =>
|
|
26
34
|
doc.structure === undefined
|
|
27
35
|
? []
|
|
28
|
-
: usesBeforeDefinition(definedTerms(doc.structure), bodyOf(doc)).map((use) =>
|
|
36
|
+
: usesBeforeDefinition(definedTerms(doc.structure), bodyOf(doc), mentionedIn(doc)).map((use) =>
|
|
37
|
+
findingAt(doc, use.offset, { term: use.term.term, defined: use.term.line }),
|
|
38
|
+
);
|
|
29
39
|
|
|
30
40
|
/** 同じ略語を、文書の中で二通りに展開している所。 */
|
|
31
41
|
export const acronymExpansionConflict: Detector = (doc): Finding[] => {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Detector, Finding, Lexicon, MarkupHeading, ProseDocument } from "../plugin.ts";
|
|
2
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
3
|
+
import { findingAt, writtenHeadings } from "./markup-finding.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 札と番号だけの見出し(メリット1、ポイント2、Benefit 3)。目次を見ても、節に何が書いてあるかが分からない。
|
|
7
|
+
* 札は語彙表 generic-heading-label が言う。題を足した見出し(メリット1:設定が要らない)は読まない。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 番号の書き方。数字、丸数字、漢数字。前に「その」や「#」、後ろに区切りの記号が付いてもよい。 */
|
|
11
|
+
const NUMBER = String.raw`(?:その)?\s*#?\s*[((]?\s*(?:[0-90-9]+|[①-⑳]|[一二三四五六七八九十]+)\s*(?:つ目)?\s*[::..。))]?`;
|
|
12
|
+
|
|
13
|
+
/** 札のどれかと番号だけで書いた見出しの形。札が無ければ undefined。 */
|
|
14
|
+
export const bareLabelPattern = (labels: Lexicon): RegExp | undefined => {
|
|
15
|
+
const words = labels.map((entry) => escapeRegExp(entry.pattern)).filter((word) => word !== "");
|
|
16
|
+
return words.length === 0 ? undefined : new RegExp(`^(?:${words.join("|")})\\s*${NUMBER}$`, "iu");
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** 札と番号だけの見出し。 */
|
|
20
|
+
export const bareLabelHeadings = (doc: ProseDocument): MarkupHeading[] => {
|
|
21
|
+
const pattern = bareLabelPattern(doc.lexicons["generic-heading-label"] ?? []);
|
|
22
|
+
return pattern === undefined ? [] : writtenHeadings(doc).filter((heading) => pattern.test(heading.text.trim()));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** 数で見る。一つなら書き手の選び方、並べば型をなぞった見出し。 */
|
|
26
|
+
export const genericHeading: Detector = (doc, options): Finding[] => {
|
|
27
|
+
const bare = bareLabelHeadings(doc);
|
|
28
|
+
if (bare.length === 0 || bare.length < options.limit) return [];
|
|
29
|
+
return bare.map((heading) => findingAt(doc, heading, { heading: heading.text.trim(), count: bare.length, limit: options.limit }));
|
|
30
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Detector, Finding } from "../plugin.ts";
|
|
2
|
+
import { quoteAround } from "./quote-around.ts";
|
|
3
|
+
|
|
4
|
+
// A single Latin letter left inside a Japanese word by the input method (おもしrおい, 対応sた): a key pressed and not converted.
|
|
5
|
+
|
|
6
|
+
/** One stray letter, and where it is in the text. */
|
|
7
|
+
export type StrayLetter = { readonly offset: number; readonly letter: string };
|
|
8
|
+
|
|
9
|
+
// Hiragana or kanji before (after katakana a letter is a label: サイズm, プランb), kana after.
|
|
10
|
+
const STRAY = /(?<=[\p{Script=Hiragana}\p{Script=Han}々])[a-z](?=[\p{Script=Hiragana}\p{Script=Katakana}ー])/gu;
|
|
11
|
+
/** Marks that make the run around the letter a path, an address or an assignment (/docs/あaい, a=b). */
|
|
12
|
+
const PATH_MARK = /[/\\=@#]/u;
|
|
13
|
+
|
|
14
|
+
const runBefore = (text: string, offset: number): string => text.slice(0, offset).split(/\s/u).at(-1) ?? "";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One lower-case letter between Japanese: hiragana or kanji before, kana after. A letter followed by a particle names
|
|
18
|
+
* something (変数xが, まずxを, をxで), so it is left alone; the particles come from the lexicon.
|
|
19
|
+
*/
|
|
20
|
+
export const strayLettersIn = (text: string, particles: readonly string[]): StrayLetter[] =>
|
|
21
|
+
[...text.matchAll(STRAY)].flatMap((match) => {
|
|
22
|
+
const namesSomething = particles.some((particle) => text.startsWith(particle, match.index + 1));
|
|
23
|
+
return namesSomething || PATH_MARK.test(runBefore(text, match.index)) ? [] : [{ offset: match.index, letter: match[0] }];
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const imeResidue: Detector = (doc): Finding[] => {
|
|
27
|
+
const text = doc.prose ?? doc.source;
|
|
28
|
+
const particles = (doc.lexicons["case-particle"] ?? []).map((entry) => entry.pattern);
|
|
29
|
+
return strayLettersIn(text, particles).map((stray) => ({
|
|
30
|
+
rule: "",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
line: 0,
|
|
33
|
+
column: 0,
|
|
34
|
+
quote: quoteAround(text, stray.offset, stray.offset + 1),
|
|
35
|
+
values: { letter: stray.letter, offset: stray.offset },
|
|
36
|
+
}));
|
|
37
|
+
};
|
package/src/detectors/lexicon.ts
CHANGED
|
@@ -212,15 +212,24 @@ export const repeatedConjunction: Detector = (doc, options): Finding[] => {
|
|
|
212
212
|
});
|
|
213
213
|
};
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* 書き手が書いた語。引用符の中だけの語(words such as "delves" and "pivotal")は、例に挙げたか人の言葉を引いたもの。
|
|
217
|
+
* 品詞があれば当たった語の並びごとに見る(活用した形も原形で当たる)。無ければ字で照らす。
|
|
218
|
+
*/
|
|
219
|
+
const writtenIn = (sentence: Sentence, entry: LexiconEntry): boolean =>
|
|
220
|
+
sentence.tokens === undefined || entry.tokens === undefined || entry.tokens.length === 0
|
|
221
|
+
? entryIn(sentence, entry) && !onlyQuoted(sentence, entry)
|
|
222
|
+
: entryRanges(sentence, entry).some((range) => !quotedRange(sentence, range));
|
|
223
|
+
|
|
215
224
|
/**
|
|
216
225
|
* AI 生成の signal。1 つでは何も言えないので、重みを足し合わせて文書の点にする。
|
|
217
226
|
* 単独で断じない。§20.2 の複合シグナルの入口で、これだけで「AI が書いた」とは言わない。
|
|
218
227
|
*/
|
|
219
228
|
export const aiTell: Detector = (doc, options): Finding[] => {
|
|
220
229
|
const entries = [...new Map((options.lexicon ?? []).map((entry) => [entry.pattern.toLowerCase(), entry])).values()];
|
|
221
|
-
const found = entries.filter((entry) => doc.sentences.some((sentence) =>
|
|
230
|
+
const found = entries.filter((entry) => doc.sentences.some((sentence) => writtenIn(sentence, entry)));
|
|
222
231
|
const score = Math.round(found.reduce((sum, entry) => sum + (entry.weight ?? 1), 0) * 10);
|
|
223
|
-
const first = doc.sentences.find((sentence) => found.some((entry) =>
|
|
232
|
+
const first = doc.sentences.find((sentence) => found.some((entry) => writtenIn(sentence, entry)));
|
|
224
233
|
if (first === undefined || score <= options.limit) return [];
|
|
225
234
|
return [
|
|
226
235
|
{
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Detector, Finding, Span } from "../plugin.ts";
|
|
2
|
+
import { codePointName } from "./invisible-character.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A character that looks like a common one but is another code point, so search, sorting and a dictionary miss it.
|
|
6
|
+
* radical: a Kangxi or supplementary radical (⼈ U+2F08 for 人), from PDF text or an input method's radical list.
|
|
7
|
+
* compatibility: a CJK compatibility ideograph (or one of its supplement) that Unicode maps to a unified one (樂 U+F914 for 樂).
|
|
8
|
+
* decomposed: a letter and a separate combining mark (か + U+3099 for が, e + U+0301 for é), from file names or text
|
|
9
|
+
* copied on macOS. spacing-mark: kana followed by the spacing ゛ or ゜, typed in place of the combined letter.
|
|
10
|
+
*/
|
|
11
|
+
export type LookalikeKind = "radical" | "compatibility" | "decomposed" | "spacing-mark";
|
|
12
|
+
|
|
13
|
+
export type Lookalike = { readonly span: Span; readonly kind: LookalikeKind; readonly written: string; readonly usual: string };
|
|
14
|
+
|
|
15
|
+
const RADICAL = /[\u2E80-\u2EFF\u2F00-\u2FDF]/u;
|
|
16
|
+
const COMPATIBILITY = /[\uF900-\uFAFF\u{2F800}-\u{2FA1F}]/u;
|
|
17
|
+
/** A letter or mark base and the combining marks after it. */
|
|
18
|
+
const WITH_MARKS = /\P{M}\p{M}+/gu;
|
|
19
|
+
/** Kana followed by the spacing voiced or semi-voiced mark (か゛, は゜). */
|
|
20
|
+
const SPACING_MARK = /[\p{Script=Hiragana}\p{Script=Katakana}][゛゜]/gu;
|
|
21
|
+
const COMBINING_OF: Readonly<Record<string, string>> = { "゛": "゙", "゜": "゚" };
|
|
22
|
+
const SINGLE_CHAR = /[\u2E80-\u2EFF\u2F00-\u2FDF\uF900-\uFAFF\u{2F800}-\u{2FA1F}]/gu;
|
|
23
|
+
|
|
24
|
+
/** A radical or compatibility ideograph, with the unified ideograph it stands for. One that maps to nothing is left alone. */
|
|
25
|
+
const singleLookalikes = (source: string): Lookalike[] =>
|
|
26
|
+
[...source.matchAll(SINGLE_CHAR)].flatMap((match) => {
|
|
27
|
+
const written = match[0];
|
|
28
|
+
const radical = RADICAL.test(written);
|
|
29
|
+
const usual = written.normalize(radical ? "NFKC" : "NFC");
|
|
30
|
+
if (usual === written || (!radical && !COMPATIBILITY.test(written))) return [];
|
|
31
|
+
return [{ span: { start: match.index, end: match.index + written.length }, kind: radical ? "radical" : "compatibility", written, usual }];
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** A base and combining marks that compose into one precomposed letter. Marks with no precomposed form are writing, not a slip. */
|
|
35
|
+
const decomposedLookalikes = (source: string): Lookalike[] =>
|
|
36
|
+
[...source.matchAll(WITH_MARKS)].flatMap((match) => {
|
|
37
|
+
const usual = match[0].normalize("NFC");
|
|
38
|
+
if (Array.from(usual).length >= Array.from(match[0]).length) return [];
|
|
39
|
+
return [{ span: { start: match.index, end: match.index + match[0].length }, kind: "decomposed", written: match[0], usual }];
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Kana and a spacing ゛ or ゜ whose combining form makes one letter (か゛ → が). */
|
|
43
|
+
const spacingMarkLookalikes = (source: string): Lookalike[] =>
|
|
44
|
+
[...source.matchAll(SPACING_MARK)].flatMap((match) => {
|
|
45
|
+
const [kana = "", mark = ""] = Array.from(match[0]);
|
|
46
|
+
const usual = `${kana}${COMBINING_OF[mark] ?? ""}`.normalize("NFC");
|
|
47
|
+
if (Array.from(usual).length !== 1) return [];
|
|
48
|
+
return [{ span: { start: match.index, end: match.index + match[0].length }, kind: "spacing-mark", written: match[0], usual }];
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const lookalikesIn = (source: string): Lookalike[] =>
|
|
52
|
+
[...singleLookalikes(source), ...decomposedLookalikes(source), ...spacingMarkLookalikes(source)].toSorted(
|
|
53
|
+
(left, right) => left.span.start - right.span.start,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const QUOTE_REACH = 20;
|
|
57
|
+
|
|
58
|
+
const quoteOf = (source: string, span: Span): string =>
|
|
59
|
+
source
|
|
60
|
+
.slice(Math.max(0, span.start - QUOTE_REACH), span.end + QUOTE_REACH)
|
|
61
|
+
.replace(/\s+/gu, " ")
|
|
62
|
+
.trim();
|
|
63
|
+
|
|
64
|
+
const codesOf = (text: string): string => Array.from(text).map(codePointName).join(" ");
|
|
65
|
+
|
|
66
|
+
export const lookalikeCharacter: Detector = (doc): Finding[] =>
|
|
67
|
+
lookalikesIn(doc.source).map((lookalike) => ({
|
|
68
|
+
rule: "",
|
|
69
|
+
severity: "warning",
|
|
70
|
+
line: 0,
|
|
71
|
+
column: 0,
|
|
72
|
+
quote: quoteOf(doc.source, lookalike.span),
|
|
73
|
+
values: { written: lookalike.written, usual: lookalike.usual, code: codesOf(lookalike.written), offset: lookalike.span.start },
|
|
74
|
+
variant: lookalike.kind,
|
|
75
|
+
}));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Detector, Finding } from "../plugin.ts";
|
|
2
|
+
import { quoteAround } from "./quote-around.ts";
|
|
3
|
+
|
|
4
|
+
// A word English always capitalises (a weekday, a month) written in lower case ("on monday"). The words come from the rule's
|
|
5
|
+
// lexicon "calendar-name"; the pattern is the capitalised form.
|
|
6
|
+
|
|
7
|
+
export type LowercaseName = { readonly offset: number; readonly written: string; readonly usual: string };
|
|
8
|
+
|
|
9
|
+
/** Characters that make the word part of a name in code, a path, an address or a tag (monday.md, /july/, @friday, #june). */
|
|
10
|
+
const NOT_PROSE_BESIDE = /[\p{L}\p{N}_/.@#:=-]/u;
|
|
11
|
+
|
|
12
|
+
/** A period or colon ends the sentence or clause unless a name continues after it (monday.md, june:2). */
|
|
13
|
+
const SENTENCE_STOP = /[.:]/u;
|
|
14
|
+
const WORD_CHAR = /[\p{L}\p{N}_]/u;
|
|
15
|
+
|
|
16
|
+
const joinsAfter = (text: string, end: number): boolean =>
|
|
17
|
+
SENTENCE_STOP.test(text.charAt(end)) ? WORD_CHAR.test(text.charAt(end + 1)) : NOT_PROSE_BESIDE.test(text.charAt(end));
|
|
18
|
+
|
|
19
|
+
/** A value after a key or a field marker (day: monday, day = monday, "day": "monday", * * monday) is data, not prose. */
|
|
20
|
+
const VALUE_MARK = /[:=*]/u;
|
|
21
|
+
const STRAIGHT_QUOTE = /["']/u;
|
|
22
|
+
const SPACE_OR_QUOTE = /[\s"']/u;
|
|
23
|
+
/** How far back to look past spaces and quotes for the key's mark. */
|
|
24
|
+
const VALUE_REACH = 8;
|
|
25
|
+
|
|
26
|
+
const markBefore = (text: string, start: number): string =>
|
|
27
|
+
Array.from(text.slice(Math.max(0, start - VALUE_REACH), start))
|
|
28
|
+
.reverse()
|
|
29
|
+
.find((char) => !SPACE_OR_QUOTE.test(char)) ?? "";
|
|
30
|
+
|
|
31
|
+
const isValue = (text: string, start: number): boolean => STRAIGHT_QUOTE.test(text.charAt(start - 1)) || VALUE_MARK.test(markBefore(text, start));
|
|
32
|
+
|
|
33
|
+
const capitalOf = (word: string): string => `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
|
|
34
|
+
|
|
35
|
+
export const lowercaseNamesIn = (text: string, names: readonly string[]): LowercaseName[] => {
|
|
36
|
+
const lower = new Map(names.map((name) => [name.toLowerCase(), capitalOf(name)]));
|
|
37
|
+
return [...text.matchAll(/\p{Ll}+/gu)].flatMap((match) => {
|
|
38
|
+
const usual = lower.get(match[0]);
|
|
39
|
+
const end = match.index + match[0].length;
|
|
40
|
+
if (usual === undefined || NOT_PROSE_BESIDE.test(text.charAt(match.index - 1)) || isValue(text, match.index) || joinsAfter(text, end)) return [];
|
|
41
|
+
return [{ offset: match.index, written: match[0], usual }];
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const lowercaseName: Detector = (doc): Finding[] => {
|
|
46
|
+
const text = doc.prose ?? doc.source;
|
|
47
|
+
return lowercaseNamesIn(
|
|
48
|
+
text,
|
|
49
|
+
(doc.lexicons["calendar-name"] ?? []).map((entry) => entry.pattern),
|
|
50
|
+
).map((name) => ({
|
|
51
|
+
rule: "",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
line: 0,
|
|
54
|
+
column: 0,
|
|
55
|
+
quote: quoteAround(text, name.offset, name.offset + name.written.length),
|
|
56
|
+
values: { written: name.written, usual: name.usual, offset: name.offset },
|
|
57
|
+
}));
|
|
58
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Detector, Finding, LexiconEntry, Paragraph, ProseDocument, Token } from "../plugin.ts";
|
|
2
|
+
import { formMinority } from "./form-minority.ts";
|
|
3
|
+
|
|
4
|
+
// A paragraph whose last sentence stops without its closing mark, in a document whose other paragraphs end with one. It
|
|
5
|
+
// takes no side: a document that ends none of its paragraphs with the mark (slides, labels) is not reported. The marks
|
|
6
|
+
// come from the language's lexicon "final-period": group "end" is the period, group "other-end" closes a sentence another
|
|
7
|
+
// way (?, !) so the paragraph is not counted, and group "note" opens a note line. The
|
|
8
|
+
// lexicon is read directly rather than as the rule's word list, since its marks are what a fixed paragraph ends with.
|
|
9
|
+
|
|
10
|
+
/** A paragraph's end, and whether it closes with a sentence-ending mark. form is "closed" or "open". */
|
|
11
|
+
export type ParagraphEnd = { readonly offset: number; readonly form: string; readonly text: string };
|
|
12
|
+
|
|
13
|
+
/** The marks the detector reads, from the lexicon. */
|
|
14
|
+
export type PeriodMarks = { readonly ends: readonly string[]; readonly otherEnds: readonly string[]; readonly notes: readonly string[] };
|
|
15
|
+
|
|
16
|
+
/** Brackets and quotes that may follow a closing mark (〜です。」). */
|
|
17
|
+
const CLOSERS = '」』))"”’';
|
|
18
|
+
/** A line that is itself a quotation closes like a sentence. */
|
|
19
|
+
const QUOTE_CLOSERS = "」』";
|
|
20
|
+
/** A paragraph ending in a colon leads into a list or a block; it is not a sentence left open. */
|
|
21
|
+
const LEAD_IN = /[::]$/u;
|
|
22
|
+
/** Two spaces in a row, half- or full-width, lay a line out like a table (an ingredient, then its amount), so it is not a sentence. */
|
|
23
|
+
const LAYOUT = /[ \u3000]{2}/u;
|
|
24
|
+
/** Fewer characters than this is a label or a caption, not a sentence. */
|
|
25
|
+
const MIN_SENTENCE_CHARS = 15;
|
|
26
|
+
/** Words that end a clause: a verb, an adjective or an auxiliary in its dictionary form ends the sentence. */
|
|
27
|
+
const PREDICATE: ReadonlySet<string> = new Set(["VERB", "ADJ", "AUX"]);
|
|
28
|
+
|
|
29
|
+
/** The line's last mark before any closing brackets and quotes is one of these. */
|
|
30
|
+
const endsWithMark = (line: string, marks: readonly string[]): boolean => {
|
|
31
|
+
const chars = Array.from(line);
|
|
32
|
+
return marks.includes(chars[chars.findLastIndex((char) => !CLOSERS.includes(char))] ?? "");
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const isClosed = (line: string, ends: readonly string[]): boolean => endsWithMark(line, ends) || QUOTE_CLOSERS.includes(line.at(-1) ?? "\0");
|
|
36
|
+
|
|
37
|
+
/** A note, a laid-out row, or a question or exclamation: not a sentence that could take the period. */
|
|
38
|
+
const isAside = (line: string, marks: PeriodMarks): boolean =>
|
|
39
|
+
marks.notes.some((note) => line.startsWith(note)) || LAYOUT.test(line) || endsWithMark(line, marks.otherEnds);
|
|
40
|
+
|
|
41
|
+
/** The paragraph's last word ends a sentence: a predicate written in its dictionary form (〜します, 〜である, 望ましい). */
|
|
42
|
+
const endsInPredicate = (paragraph: Paragraph): boolean => {
|
|
43
|
+
const last = (paragraph.sentences.at(-1)?.tokens ?? []).filter((token: Token) => token.surface.trim() !== "").at(-1);
|
|
44
|
+
return last !== undefined && PREDICATE.has(last.pos) && last.lemma === last.surface;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** How a paragraph ends, or undefined when its last line is not a sentence (a label, a lead-in, a note, a laid-out row). */
|
|
48
|
+
export const paragraphEnd = (text: string, paragraph: Paragraph, marks: PeriodMarks): ParagraphEnd | undefined => {
|
|
49
|
+
const body = text.slice(paragraph.span.start, paragraph.span.end).trimEnd();
|
|
50
|
+
const last = body.split("\n").at(-1)?.trim() ?? "";
|
|
51
|
+
if (Array.from(last).length < MIN_SENTENCE_CHARS || LEAD_IN.test(last)) return undefined;
|
|
52
|
+
const offset = paragraph.span.start + body.length - 1;
|
|
53
|
+
if (isAside(last, marks)) return undefined;
|
|
54
|
+
if (isClosed(last, marks.ends)) return { offset, form: "closed", text: last };
|
|
55
|
+
// A closing parenthetical after the sentence (〜します(別紙を参照する)) is not where the period goes.
|
|
56
|
+
return !CLOSERS.includes(last.at(-1) ?? "\0") && endsInPredicate(paragraph) ? { offset, form: "open", text: last } : undefined;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const marksOf = (lexicon: readonly LexiconEntry[]): PeriodMarks => ({
|
|
60
|
+
ends: lexicon.filter((entry) => entry.group === "end").map((entry) => entry.pattern),
|
|
61
|
+
otherEnds: lexicon.filter((entry) => entry.group === "other-end").map((entry) => entry.pattern),
|
|
62
|
+
notes: lexicon.filter((entry) => entry.group === "note").map((entry) => entry.pattern),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const missingFinalPeriod: Detector = (doc: ProseDocument, options): Finding[] => {
|
|
66
|
+
const text = doc.prose ?? doc.source;
|
|
67
|
+
const marks = marksOf(doc.lexicons["final-period"] ?? []);
|
|
68
|
+
const ends = doc.paragraphs.flatMap((paragraph) => paragraphEnd(text, paragraph, marks) ?? []);
|
|
69
|
+
const minority = formMinority(ends, options.limit);
|
|
70
|
+
if (minority === undefined || minority.usual.form !== "closed") return [];
|
|
71
|
+
return minority.odd.map((end) => ({
|
|
72
|
+
rule: "",
|
|
73
|
+
severity: "info",
|
|
74
|
+
line: 0,
|
|
75
|
+
column: 0,
|
|
76
|
+
quote: end.text,
|
|
77
|
+
values: { count: minority.odd.length, of: ends.length, offset: end.offset },
|
|
78
|
+
}));
|
|
79
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Detector, Finding } from "../plugin.ts";
|
|
2
|
+
import { quoteAround } from "./quote-around.ts";
|
|
3
|
+
|
|
4
|
+
// A comma between two words with no space after it ("address,and"): a slip in English prose. A run of values joined by
|
|
5
|
+
// commas ("blue,black,brown"), and anything inside an address, a path or an assignment, is data, not prose.
|
|
6
|
+
|
|
7
|
+
/** One comma with no space after it, where it is, and the two words it joins as written. */
|
|
8
|
+
export type TightComma = { readonly offset: number; readonly written: string };
|
|
9
|
+
|
|
10
|
+
/** A word of three letters or more (a possessive 's may follow), a comma, and a lower-case word of three letters or more. */
|
|
11
|
+
const JOINED = /(?<![\p{L}\p{N}])[A-Za-z]{3,}(?:['’]s)?,[a-z]{3,}(?![\p{L}\p{N}])/u;
|
|
12
|
+
/** Characters that make a run of text an address, a path, a parameter or data (a=b, a/b, a.b, @a, #a, a_b, 3,abc). */
|
|
13
|
+
const DATA_MARK = /[/\\=@#:.&?_|+*~$%^<>{}[\]\p{N}]/u;
|
|
14
|
+
/** Opening and closing marks a word of prose may carry ("(and,then)"), and the sentence's own end ("and,then."). */
|
|
15
|
+
const OPENERS = "(\"'“‘[";
|
|
16
|
+
const CLOSERS = ")\"'”’].,;!?";
|
|
17
|
+
/** A run quoted whole, inside brackets or not ("true,false", ("true,false")): a value written out, not two words of the sentence. */
|
|
18
|
+
const QUOTED_WHOLE = /^[([]*["'“‘].*["'”’][)\].,;!?]*$/u;
|
|
19
|
+
/** A run of text between whitespace. */
|
|
20
|
+
const RUN = /\S+/gu;
|
|
21
|
+
const COMMA = ",";
|
|
22
|
+
|
|
23
|
+
/** The run without the marks around it, trimmed by scanning rather than by a pattern that could backtrack. */
|
|
24
|
+
const bareOf = (run: string): string => {
|
|
25
|
+
const chars = run.split("");
|
|
26
|
+
const start = chars.findIndex((char) => !OPENERS.includes(char));
|
|
27
|
+
const end = chars.findLastIndex((char) => !CLOSERS.includes(char));
|
|
28
|
+
return start === -1 || end < start ? "" : run.slice(start, end + 1);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** A run holding one comma and nothing that marks it as data. */
|
|
32
|
+
const isProseRun = (run: string): boolean => {
|
|
33
|
+
const bare = bareOf(run);
|
|
34
|
+
return bare.split(COMMA).length === 2 && !DATA_MARK.test(bare) && !QUOTED_WHOLE.test(run);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Each run is read once, so a long line of values costs one pass. */
|
|
38
|
+
export const tightCommasIn = (text: string): TightComma[] =>
|
|
39
|
+
[...text.matchAll(RUN)].flatMap((run) => {
|
|
40
|
+
const joined = isProseRun(run[0]) ? JOINED.exec(run[0]) : null;
|
|
41
|
+
return joined === null ? [] : [{ offset: run.index + joined.index + joined[0].indexOf(COMMA), written: joined[0] }];
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const missingSpaceAfterComma: Detector = (doc): Finding[] => {
|
|
45
|
+
const text = doc.prose ?? doc.source;
|
|
46
|
+
return tightCommasIn(text).map((comma) => ({
|
|
47
|
+
rule: "",
|
|
48
|
+
severity: "warning",
|
|
49
|
+
line: 0,
|
|
50
|
+
column: 0,
|
|
51
|
+
quote: quoteAround(text, comma.offset, comma.offset + 1),
|
|
52
|
+
values: { written: comma.written, offset: comma.offset },
|
|
53
|
+
}));
|
|
54
|
+
};
|
package/src/detectors/signals.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { evidenceSpans, hasNumeral, startsWithin } from "./concrete-evidence.ts"
|
|
|
11
11
|
import { letteredIndexEntries } from "./lettered-index.ts";
|
|
12
12
|
import { hasPredicateIn } from "./gram-predicate.ts";
|
|
13
13
|
import { nameSpans, touchesAny } from "../team-names.ts";
|
|
14
|
+
import { referenceListSpans } from "../reference-lists.ts";
|
|
14
15
|
import type { Detector, Finding, ProseDocument, Section, Sentence } from "../plugin.ts";
|
|
15
16
|
|
|
16
17
|
const PER = 1000;
|
|
@@ -250,9 +251,12 @@ const firstHits = (hits: readonly AcronymHit[]): Map<string, Hit> => {
|
|
|
250
251
|
return seen;
|
|
251
252
|
};
|
|
252
253
|
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
/**
|
|
255
|
+
* 範囲の外の略語。日付の中の月(SEP 01, 2022)は略語ではない。文献一覧の誌名や会議名(Appeared in: LREC 2008.)は
|
|
256
|
+
* 引いた文献の書き方で、本文が説明する略語ではない。
|
|
257
|
+
*/
|
|
258
|
+
const outsideSpans = (hits: readonly AcronymHit[], spans: readonly Span[]): AcronymHit[] =>
|
|
259
|
+
hits.filter(({ hit }) => !spans.some((span) => span.start <= hit.offset && hit.offset < span.end));
|
|
256
260
|
|
|
257
261
|
/**
|
|
258
262
|
* どこか 1 か所で展開してあればよい。初出が節の見出し代わりの語(「5.3. DPA.」)で、
|
|
@@ -300,11 +304,11 @@ export const undefinedAcronym: Detector = (doc, options): Finding[] => {
|
|
|
300
304
|
const headings = doc.sections.map((section) => section.heading);
|
|
301
305
|
const explainedAlone = (word: string): boolean =>
|
|
302
306
|
common.has(word) || entries.has(word) || isExpanded(body, word, expandedAt) || headings.some((heading) => expandsInHeading(heading, word, expandedAt));
|
|
303
|
-
const hits = acronymsOf(doc, notationOf(doc));
|
|
307
|
+
const hits = outsideSpans(acronymsOf(doc, notationOf(doc)), referenceListSpans(doc.source, patternsOf(doc, "reference-list-heading")));
|
|
304
308
|
const unexplained = new Set([...firstHits(hits).keys()].filter((acronym) => !isExplained(acronym, explainedAlone)));
|
|
305
309
|
// 日付を読むには文書の木を作る。上限に届かない文書では作らない。
|
|
306
310
|
if (unexplained.size < options.limit) return [];
|
|
307
|
-
const bare = [...firstHits(
|
|
311
|
+
const bare = [...firstHits(outsideSpans(hits, dateSpans(doc)))].filter(([acronym]) => unexplained.has(acronym));
|
|
308
312
|
if (bare.length < options.limit) return [];
|
|
309
313
|
return bare.map(([acronym, hit]) => ({
|
|
310
314
|
rule: "undefined-acronym",
|
|
@@ -6,6 +6,7 @@ import { dateOrderBreaks } from "../structure/date-order.ts";
|
|
|
6
6
|
import { totalMismatches, type Amount } from "../structure/total.ts";
|
|
7
7
|
import { rangeFrameOf, reversedRanges, type DatedSpan, type RangeWords } from "../structure/date-range.ts";
|
|
8
8
|
import { percentSumMismatches, type ShareWords } from "../structure/percent-sum.ts";
|
|
9
|
+
import { isQuotedAlone } from "../quoted-span.ts";
|
|
9
10
|
|
|
10
11
|
const QUOTE_LENGTH = 80;
|
|
11
12
|
|
|
@@ -42,22 +43,43 @@ const dayName = (doc: ProseDocument, day: unknown): string => {
|
|
|
42
43
|
return doc.lexicons["weekday"]?.[index]?.pattern ?? String(index);
|
|
43
44
|
};
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
const datedSpans = (tree: NonNullable<ProseDocument["structure"]>): DatedSpan[] =>
|
|
47
|
+
inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ offset: node.span.start, end: node.span.end, value: String(node.attrs["value"]) }] : []));
|
|
48
|
+
|
|
49
|
+
/** 引用符の中で日付の横にあってよい語は、曜日の一語だけ("Monday, December 5, 2026"、「2026年12月5日(月)」)。 */
|
|
50
|
+
const WEEKDAY_WORDS = 1;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 引用符で日付だけを挙げたもの("Monday, December 5, 2026" falls on a Saturday)は、誤りの例で、文書の日付ではない。
|
|
54
|
+
* from から until の日付の終わりまでを見る。
|
|
55
|
+
*/
|
|
56
|
+
const quotedDates = (doc: ProseDocument, from: DatedSpan | undefined, until: DatedSpan | undefined, wordsBesides = 0): boolean =>
|
|
57
|
+
from !== undefined && until !== undefined && isQuotedAlone(doc.source, { start: from.offset, end: until.end }, wordsBesides);
|
|
58
|
+
|
|
59
|
+
const dateAt = (dates: readonly DatedSpan[], offset: number): number => dates.findIndex((date) => date.offset === offset);
|
|
60
|
+
|
|
61
|
+
export const dateWeekdayMismatch: Detector = (doc): Finding[] => {
|
|
62
|
+
if (doc.structure === undefined) return [];
|
|
63
|
+
const dates = datedSpans(doc.structure);
|
|
64
|
+
return weekdayMismatches(doc.structure)
|
|
65
|
+
.filter((issue) => {
|
|
66
|
+
const date = dates[dateAt(dates, issue.offset)];
|
|
67
|
+
return !quotedDates(doc, date, date, WEEKDAY_WORDS);
|
|
68
|
+
})
|
|
69
|
+
.map((issue) => ({
|
|
70
|
+
rule: "date-weekday-mismatch",
|
|
71
|
+
severity: "error",
|
|
72
|
+
line: 0,
|
|
73
|
+
column: 0,
|
|
74
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
75
|
+
values: {
|
|
76
|
+
date: String(issue.values["date"]),
|
|
77
|
+
written: dayName(doc, issue.values["written"]),
|
|
78
|
+
actual: dayName(doc, issue.values["actual"]),
|
|
79
|
+
offset: issue.offset,
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
};
|
|
61
83
|
|
|
62
84
|
/** 木の日付を、原文の位置と一緒に並べる。 */
|
|
63
85
|
const datedPoints = (tree: NonNullable<ProseDocument["structure"]>): { offset: number; value: string }[] =>
|
|
@@ -99,9 +121,6 @@ export const totalMismatch: Detector = (doc): Finding[] =>
|
|
|
99
121
|
values: { ...issue.values, offset: issue.offset },
|
|
100
122
|
}));
|
|
101
123
|
|
|
102
|
-
const datedSpans = (tree: NonNullable<ProseDocument["structure"]>): DatedSpan[] =>
|
|
103
|
-
inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ offset: node.span.start, end: node.span.end, value: String(node.attrs["value"]) }] : []));
|
|
104
|
-
|
|
105
124
|
const rangeWordsOf = (doc: ProseDocument): RangeWords => ({
|
|
106
125
|
connectors: (doc.lexicons["range-connector"] ?? []).map((entry) => entry.pattern),
|
|
107
126
|
openers: (doc.lexicons["range-opener"] ?? []).map((entry) => entry.pattern),
|
|
@@ -111,17 +130,23 @@ const rangeWordsOf = (doc: ProseDocument): RangeWords => ({
|
|
|
111
130
|
});
|
|
112
131
|
|
|
113
132
|
/** 期間の終わりが始まりより前。範囲の記号と語は言語パッケージの語彙表(range-connector、range-opener、range-closer)から取る。 */
|
|
114
|
-
export const dateRangeReversed: Detector = (doc): Finding[] =>
|
|
115
|
-
doc.structure === undefined
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
export const dateRangeReversed: Detector = (doc): Finding[] => {
|
|
134
|
+
if (doc.structure === undefined) return [];
|
|
135
|
+
const dates = datedSpans(doc.structure);
|
|
136
|
+
return reversedRanges(doc.source, dates, rangeWordsOf(doc))
|
|
137
|
+
.filter((issue) => {
|
|
138
|
+
const start = dateAt(dates, issue.offset);
|
|
139
|
+
return !quotedDates(doc, dates[start], dates[start + 1]);
|
|
140
|
+
})
|
|
141
|
+
.map((issue) => ({
|
|
142
|
+
rule: "date-range-reversed",
|
|
143
|
+
severity: "warning",
|
|
144
|
+
line: 0,
|
|
145
|
+
column: 0,
|
|
146
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
147
|
+
values: { ...issue.values, offset: issue.offset },
|
|
148
|
+
}));
|
|
149
|
+
};
|
|
125
150
|
|
|
126
151
|
const shareWordsOf = (doc: ProseDocument): ShareWords => ({
|
|
127
152
|
labels: (doc.lexicons["share-label"] ?? []).map((entry) => entry.pattern),
|