hanlint 0.0.7 → 0.0.8
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/LICENSE +21 -0
- package/README.md +23 -5
- package/data/adjectiveStems.txt +118 -0
- package/data/baselineAnchors.txt +3 -0
- package/data/blindEvaluation.schema.json +26 -0
- package/data/bridgeOpeners.txt +10 -0
- package/data/candidateActionNouns.txt +39 -0
- package/data/candidateSplitEndings.txt +13 -0
- package/data/cliches.json +1 -1
- package/data/confusable.json +2 -2
- package/data/deixis.txt +10 -3
- package/data/draftHistory.txt +10 -0
- package/data/easyWords.json +0 -42
- package/data/enoughMarkers.txt +6 -0
- package/data/entailmentPredictions.schema.json +52 -0
- package/data/euiNouns.txt +3 -4
- package/data/evidenceEntailmentBenchmark.schema.json +126 -0
- package/data/exemplars.json +347 -0
- package/data/holeKinds.json +1 -2
- package/data/iVerbStems.txt +26 -0
- package/data/inNouns.txt +147 -0
- package/data/irregularStems.txt +64 -0
- package/data/nonNouns.txt +153 -0
- package/data/panelAssignmentReview.schema.json +117 -0
- package/data/panelJudgePredictions.schema.json +78 -0
- package/data/panelReviewBatch.schema.json +79 -0
- package/data/panelTrialSet.schema.json +44 -0
- package/data/patterns.json +114 -0
- package/data/profiles.json +1 -0
- package/data/readerCalls.txt +12 -7
- package/data/redundantPair.json +1 -1
- package/data/rieulStems.txt +30 -0
- package/data/ruleCategories.json +56 -0
- package/data/ruleDocs.json +29 -17
- package/data/ruleMechanisms.json +56 -0
- package/data/spacing.json +0 -12
- package/data/spelling.json +6 -24
- package/data/translationese.json +10 -0
- package/data/verbTails.txt +17 -0
- package/data/version.json +1 -1
- package/data/writingBrief.schema.json +64 -0
- package/data/writingBriefV2.schema.json +116 -0
- package/data/writingTrial.schema.json +36 -0
- package/package.json +13 -5
- package/src/analysis/grammar/ending.js +336 -0
- package/src/analysis/grammar/hangul.js +94 -0
- package/src/analysis/grammar/index.js +5 -0
- package/src/analysis/grammar/josa.js +73 -0
- package/src/analysis/grammar/register.js +115 -0
- package/src/analysis/grammar/voice.js +43 -0
- package/src/analysis/index.js +4 -31
- package/src/analysis/josa.js +2 -0
- package/src/analysis/{surface/splitSentences.js → splitSentences.js} +1 -1
- package/src/analysis/{surface/tokenize.js → tokenize.js} +70 -13
- package/src/baseline/store.js +114 -0
- package/src/cli/main.js +506 -58
- package/src/cli/walk.js +30 -0
- package/src/cli/welcome.js +52 -0
- package/src/config/settings.js +103 -8
- package/src/data/exemplars.js +177 -0
- package/src/data/load.js +10 -0
- package/src/data/operations.js +278 -0
- package/src/data/patches.js +125 -0
- package/src/data/patterns.js +48 -0
- package/src/data/profiles.js +102 -0
- package/src/document/model.js +2 -0
- package/src/document/parseMarkdown.js +84 -11
- package/src/edit/applyFixes.js +0 -0
- package/src/fingerprint/build.js +50 -32
- package/src/{rules/shared → fingerprint}/codeBlocks.js +11 -11
- package/src/fingerprint/codeMarkers.js +73 -0
- package/src/fingerprint/markers.js +9 -0
- package/src/fingerprint/readerState.js +126 -0
- package/src/fingerprint/sourceSentence.js +37 -0
- package/src/fingerprint/topics.js +1 -1
- package/src/index.js +2 -16
- package/src/report/compactReport.js +6 -1
- package/src/report/fingerprintJson.js +8 -3
- package/src/report/jsonReport.js +50 -3
- package/src/report/operationMatch.js +31 -0
- package/src/report/patchMatch.js +44 -0
- package/src/report/registerMatch.js +40 -0
- package/src/report/textReport.js +41 -5
- package/src/rules/code/duplicateBlock.js +4 -3
- package/src/rules/code/firstResultDistance.js +1 -0
- package/src/rules/code/inputFileSource.js +23 -68
- package/src/rules/code/installImport.js +4 -4
- package/src/rules/code/platformApi.js +7 -6
- package/src/rules/document/countMismatch.js +22 -6
- package/src/rules/document/enoughOnce.js +25 -0
- package/src/rules/document/fieldEcho.js +38 -0
- package/src/rules/document/keywordHeading.js +27 -0
- package/src/rules/document/keywordMissing.js +1 -0
- package/src/rules/document/noQuestion.js +5 -1
- package/src/rules/document/promiseRecall.js +4 -2
- package/src/rules/document/tableOddCell.js +60 -0
- package/src/rules/finding.js +24 -2
- package/src/rules/orthography/confusable.js +1 -0
- package/src/rules/orthography/spacing.js +1 -0
- package/src/rules/orthography/spelling.js +1 -0
- package/src/rules/paragraph/factListParagraph.js +1 -0
- package/src/rules/paragraph/paraFragment.js +21 -11
- package/src/rules/paragraph/topicBreak.js +1 -0
- package/src/rules/registry.js +72 -3
- package/src/rules/sentence/cliche.js +1 -0
- package/src/rules/sentence/connectorRepeat.js +8 -7
- package/src/rules/sentence/danglingDeixis.js +8 -3
- package/src/rules/sentence/dash.js +2 -1
- package/src/rules/sentence/deixis.js +7 -3
- package/src/rules/sentence/doubleNegative.js +4 -1
- package/src/rules/sentence/doublePassive.js +38 -1
- package/src/rules/sentence/draftHistory.js +24 -0
- package/src/rules/sentence/ellipsis.js +17 -3
- package/src/rules/sentence/endingRepeat.js +6 -2
- package/src/rules/sentence/euiChain.js +3 -2
- package/src/rules/sentence/fillerOpener.js +1 -0
- package/src/rules/sentence/hardWord.js +1 -0
- package/src/rules/sentence/imperativePeriod.js +2 -1
- package/src/rules/sentence/japaneseLoan.js +1 -0
- package/src/rules/sentence/longSentence.js +3 -1
- package/src/rules/sentence/negationRedefine.js +10 -1
- package/src/rules/sentence/nounPile.js +1 -0
- package/src/rules/sentence/numberOrphan.js +45 -0
- package/src/rules/sentence/outsideProfile.js +68 -0
- package/src/rules/sentence/redundantPair.js +1 -0
- package/src/rules/sentence/translationese.js +1 -0
- package/src/rules/shared/candidates.js +129 -0
- package/src/rules/shared/dictionaryRule.js +12 -2
- package/src/rules/shared/localAntecedent.js +12 -0
- package/src/rules/shared/repeat.js +48 -0
- package/src/rules/structure/blockUnread.js +33 -0
- package/src/rules/structure/bridgeRepeat.js +40 -0
- package/src/rules/structure/emojiBullet.js +1 -0
- package/src/rules/structure/headingQuestion.js +19 -0
- package/src/rules/structure/headingSentence.js +9 -4
- package/src/rules/structure/headingSkip.js +1 -0
- package/src/rules/structure/headingUniform.js +9 -17
- package/src/rules/structure/introImage.js +19 -0
- package/src/rules/structure/introLong.js +1 -0
- package/src/rules/structure/loneSubheading.js +42 -0
- package/src/rules/structure/moreLater.js +48 -0
- package/src/rules/structure/sectionNoProse.js +1 -0
- package/src/rules/structure/sectionResult.js +3 -2
- package/src/text.js +19 -0
- package/src/rules/document/readerAbsent.js +0 -11
- package/src/rules/shared/runs.js +0 -22
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 반복 기제. 같은 모양이 창 안에서 이어지거나 (runsOf) 한 모양이 창을 채우는 (shareOf) 자리를 센다.
|
|
4
|
+
* 파이썬 rules/shared/repeat.py 와 같다. 규칙은 열쇠와 창과 임계만 선언하고 셈은 여기서 한다.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 같은 값이 minLength 개 이상 이어지는 구간을 [시작 위치, 길이, 값] 으로 준다.
|
|
9
|
+
* @template T
|
|
10
|
+
* @param {T[]} values
|
|
11
|
+
* @param {number} minLength
|
|
12
|
+
* @returns {[number, number, T][]}
|
|
13
|
+
*/
|
|
14
|
+
export function runsOf(values, minLength) {
|
|
15
|
+
/** @type {[number, number, T][]} */
|
|
16
|
+
const runs = [];
|
|
17
|
+
let start = 0;
|
|
18
|
+
while (start < values.length) {
|
|
19
|
+
let end = start;
|
|
20
|
+
while (end + 1 < values.length && values[end + 1] === values[start]) end += 1;
|
|
21
|
+
const length = end - start + 1;
|
|
22
|
+
if (length >= minLength) runs.push([start, length, values[start]]);
|
|
23
|
+
start = end + 1;
|
|
24
|
+
}
|
|
25
|
+
return runs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 가장 많은 값과 그 수와 전체 수. 같은 수면 먼저 나온 값이 이긴다. 빈 열이면 [null, 0, 0].
|
|
30
|
+
* @template T
|
|
31
|
+
* @param {T[]} values
|
|
32
|
+
* @returns {[T | null, number, number]}
|
|
33
|
+
*/
|
|
34
|
+
export function shareOf(values) {
|
|
35
|
+
/** @type {Map<T, number>} */
|
|
36
|
+
const counts = new Map();
|
|
37
|
+
for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
38
|
+
/** @type {T | null} */
|
|
39
|
+
let best = null;
|
|
40
|
+
let count = 0;
|
|
41
|
+
for (const [value, seen] of counts) {
|
|
42
|
+
if (seen > count) {
|
|
43
|
+
best = value;
|
|
44
|
+
count = seen;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [best, count, values.length];
|
|
48
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { PROSE } from "../../document/model.js";
|
|
3
|
+
import { NOTICE, SECTION, finding } from "../finding.js";
|
|
4
|
+
|
|
5
|
+
export const name = "blockUnread";
|
|
6
|
+
export const mechanism = "threshold";
|
|
7
|
+
const OUTPUT_LANGUAGES = ["", "text", "output", "console"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
11
|
+
* @param {import("../../config/settings.js").Config} config
|
|
12
|
+
*/
|
|
13
|
+
export function run(doc, config) {
|
|
14
|
+
void config;
|
|
15
|
+
/** @type {Map<number, string>} */
|
|
16
|
+
const languages = new Map(doc.codeBlocks.map((block) => [block.index, block.language]));
|
|
17
|
+
const outputs = new Set([...languages].filter(([, language]) => OUTPUT_LANGUAGES.includes(language)).map(([index]) => index));
|
|
18
|
+
if (!outputs.size) return [];
|
|
19
|
+
const blocks = [...doc.blocks].sort((a, b) => a.index - b.index);
|
|
20
|
+
const findings = [];
|
|
21
|
+
for (let position = 0; position < blocks.length; position += 1) {
|
|
22
|
+
const block = blocks[position];
|
|
23
|
+
if (!outputs.has(block.index) || position === 0) continue;
|
|
24
|
+
const previous = languages.get(blocks[position - 1].index);
|
|
25
|
+
if (previous === undefined || OUTPUT_LANGUAGES.includes(previous)) continue;
|
|
26
|
+
const following = position + 1 < blocks.length ? blocks[position + 1] : null;
|
|
27
|
+
if (following !== null && following.kind === PROSE) continue;
|
|
28
|
+
findings.push(
|
|
29
|
+
finding(name, block.startLine, block.text.includes("\n") ? block.text.split("\n")[1] : block.text, "출력을 붙여 놓고 읽어 주지 않았다. 어느 부분이 무엇인지 짚는 문장을 바로 아래에 붙인다", null, NOTICE, SECTION, block.index),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return findings;
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { matchedTexts } from "../../fingerprint/markers.js";
|
|
3
|
+
import { DOCUMENT, finding } from "../finding.js";
|
|
4
|
+
|
|
5
|
+
export const name = "bridgeRepeat";
|
|
6
|
+
export const mechanism = "repeat";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 절을 닫는 문장이 이음 표지 (이번에는, 이제, 다음으로) 로 시작하는 절이 bridgeRepeatMin 개 이상인 글.
|
|
10
|
+
* 파이썬 rules/structure/bridgeRepeat.py 와 같다. 표지 목록은 data/bridgeOpeners.txt.
|
|
11
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
12
|
+
* @param {import("../../config/settings.js").Config} config
|
|
13
|
+
*/
|
|
14
|
+
export function run(doc, config) {
|
|
15
|
+
/** @type {[import("../../fingerprint/build.js").SentencePrint, string][]} */
|
|
16
|
+
const closers = [];
|
|
17
|
+
for (const section of doc.bodySections) {
|
|
18
|
+
if (!section.paragraphs.length) continue;
|
|
19
|
+
const sentences = section.paragraphs[section.paragraphs.length - 1].sentences;
|
|
20
|
+
if (!sentences.length) continue;
|
|
21
|
+
const last = sentences[sentences.length - 1];
|
|
22
|
+
const found = matchedTexts(last.text, "bridgeOpeners.txt");
|
|
23
|
+
if (found.length) closers.push([last, found[0]]);
|
|
24
|
+
}
|
|
25
|
+
if (closers.length < config.bridgeRepeatMin) return [];
|
|
26
|
+
const markers = [...new Set(closers.map(([, marker]) => marker))].sort();
|
|
27
|
+
const [first] = closers[0];
|
|
28
|
+
return [
|
|
29
|
+
finding(
|
|
30
|
+
name,
|
|
31
|
+
first.line,
|
|
32
|
+
first.text,
|
|
33
|
+
`절 ${closers.length}개가 \`${markers.join(", ")}\` 같은 이음 표지로 시작하는 문장으로 닫힌다. 예고가 절마다 되풀이되면 틀이다. 방금 만든 결과를 이름으로 부르고 아직 못 하는 일을 적는다`,
|
|
34
|
+
null,
|
|
35
|
+
"error",
|
|
36
|
+
DOCUMENT,
|
|
37
|
+
-1,
|
|
38
|
+
),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
@@ -3,6 +3,7 @@ import { LIST } from "../../document/model.js";
|
|
|
3
3
|
import { DOCUMENT, finding } from "../finding.js";
|
|
4
4
|
|
|
5
5
|
export const name = "emojiBullet";
|
|
6
|
+
export const mechanism = "dictionary";
|
|
6
7
|
// 이모지 범위. 코드포인트로 만든다 (도구가 이스케이프를 건드리지 않게). 아스트랄 범위라 u 플래그가 필요하다.
|
|
7
8
|
const EMOJI =
|
|
8
9
|
"[" +
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { DOCUMENT, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "headingQuestion";
|
|
5
|
+
export const mechanism = "repeat";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
9
|
+
* @param {import("../../config/settings.js").Config} config
|
|
10
|
+
*/
|
|
11
|
+
export function run(doc, config) {
|
|
12
|
+
const headings = doc.headings.filter(([level]) => level === 2);
|
|
13
|
+
if (headings.length < 3) return [];
|
|
14
|
+
const asking = headings.filter(([, text]) => text.trimEnd().endsWith("?")).map(([, text]) => text);
|
|
15
|
+
if (asking.length / headings.length <= config.headingQuestionRatio) return [];
|
|
16
|
+
return [
|
|
17
|
+
finding(name, headings[0][2], asking.join(" / "), `H2 ${headings.length}개 중 ${asking.length}개가 물음표로 끝난다. 목차가 과정이 아니라 FAQ 로 읽힌다. 형태를 섞는다`, null, "error", DOCUMENT, -1),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
import { DOCUMENT, finding } from "../finding.js";
|
|
3
3
|
|
|
4
4
|
export const name = "headingSentence";
|
|
5
|
-
const
|
|
5
|
+
export const mechanism = "threshold";
|
|
6
|
+
const SENTENCE_ENDINGS = ["니다", "한다", "해요", "세요", "십시오", "합시다", "."];
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
9
13
|
const findings = [];
|
|
10
|
-
for (const [, text, line] of doc.headings) {
|
|
14
|
+
for (const [level, text, line] of doc.headings) {
|
|
15
|
+
if (level > config.headingSentenceMaxLevel) continue;
|
|
11
16
|
const trimmed = text.trimEnd();
|
|
12
17
|
if (SENTENCE_ENDINGS.some((ending) => trimmed.endsWith(ending))) {
|
|
13
18
|
findings.push(finding(name, line, text, "절 제목이 본문 문장을 복사했다. 대상이나 질문을 짧게 쓴다", null, "error", DOCUMENT, -1));
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import { DOCUMENT, finding } from "../finding.js";
|
|
3
|
+
import { shareOf } from "../shared/repeat.js";
|
|
3
4
|
|
|
4
5
|
export const name = "headingUniform";
|
|
6
|
+
export const mechanism = "repeat";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
@@ -17,23 +19,13 @@ export function run(doc, config) {
|
|
|
17
19
|
return !(char >= "0" && char <= "9");
|
|
18
20
|
});
|
|
19
21
|
if (eligible.length < 3) return [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
let best = "";
|
|
29
|
-
let count = -1;
|
|
30
|
-
for (const [char, n] of lastChars) {
|
|
31
|
-
if (n > count) {
|
|
32
|
-
best = char;
|
|
33
|
-
count = n;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (count / eligible.length >= config.headingUniformRatio) {
|
|
22
|
+
const [best, count, total] = shareOf(
|
|
23
|
+
eligible.map(([, text]) => {
|
|
24
|
+
const trimmed = text.trimEnd();
|
|
25
|
+
return trimmed[trimmed.length - 1];
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
if (count / total >= config.headingUniformRatio) {
|
|
37
29
|
return [
|
|
38
30
|
finding(name, eligible[0][2], eligible.map(([, text]) => text).join(" / "), `H2 ${eligible.length}개 중 ${count}개가 \`${best}\` 로 끝난다. 목차가 한 어미로 끝나면 과정이 아니라 나열로 읽힌다. 형태를 섞는다`, null, "error", DOCUMENT, -1),
|
|
39
31
|
];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { IMAGE } from "../../document/model.js";
|
|
3
|
+
import { SECTION, finding } from "../finding.js";
|
|
4
|
+
|
|
5
|
+
export const name = "introImage";
|
|
6
|
+
export const mechanism = "threshold";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
if (!doc.bodySections.length) return [];
|
|
14
|
+
const count = doc.intro.blockKinds.filter((kind) => kind === IMAGE).length;
|
|
15
|
+
if (count <= config.introMaxImages) return [];
|
|
16
|
+
return [
|
|
17
|
+
finding(name, doc.intro.startLine, doc.intro.title || "도입", `도입에 그림이 ${count}장이다. ${config.introMaxImages}장을 넘지 않는다. 나머지는 그것을 설명하는 절로 내린다`, null, "error", SECTION, doc.intro.index),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { topicsOf } from "../../fingerprint/topics.js";
|
|
3
|
+
import { NOTICE, SECTION, finding } from "../finding.js";
|
|
4
|
+
|
|
5
|
+
export const name = "loneSubheading";
|
|
6
|
+
export const mechanism = "contrast";
|
|
7
|
+
|
|
8
|
+
/** (부모 H2 제목, 외동 H3 제목, 그 줄). H2 하나 아래 H3 이 정확히 하나일 때만. @param {[number, string, number][]} headings */
|
|
9
|
+
function onlyChild(headings) {
|
|
10
|
+
/** @type {[string, string, number][]} */
|
|
11
|
+
const found = [];
|
|
12
|
+
for (let position = 0; position < headings.length; position += 1) {
|
|
13
|
+
const [level, text] = headings[position];
|
|
14
|
+
if (level !== 2) continue;
|
|
15
|
+
/** @type {[string, number][]} */
|
|
16
|
+
const children = [];
|
|
17
|
+
for (const [childLevel, childText, childLine] of headings.slice(position + 1)) {
|
|
18
|
+
if (childLevel <= 2) break;
|
|
19
|
+
if (childLevel === 3) children.push([childText, childLine]);
|
|
20
|
+
}
|
|
21
|
+
if (children.length === 1) found.push([text, children[0][0], children[0][1]]);
|
|
22
|
+
}
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
28
|
+
* @param {import("../../config/settings.js").Config} config
|
|
29
|
+
*/
|
|
30
|
+
export function run(doc, config) {
|
|
31
|
+
void config;
|
|
32
|
+
const findings = [];
|
|
33
|
+
for (const [parent, child, line] of onlyChild(doc.headings)) {
|
|
34
|
+
const childTopics = topicsOf(child);
|
|
35
|
+
const parentTopics = topicsOf(parent);
|
|
36
|
+
if (!childTopics.size || ![...childTopics].every((word) => parentTopics.has(word))) continue;
|
|
37
|
+
findings.push(
|
|
38
|
+
finding(name, line, child, `\`${parent}\` 아래 소제목이 이것 하나뿐이고 절 제목에 없는 말이 하나도 없다. 소제목을 지우고 본문을 올린다`, null, NOTICE, SECTION, -1),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return findings;
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { LIST } from "../../document/model.js";
|
|
3
|
+
import { plainText } from "../../document/plainText.js";
|
|
4
|
+
import { NOTICE, SECTION, finding } from "../finding.js";
|
|
5
|
+
|
|
6
|
+
export const name = "moreLater";
|
|
7
|
+
export const mechanism = "threshold";
|
|
8
|
+
const BULLET = /^\s*(?:[-*+]|\d+[.)])\s+/;
|
|
9
|
+
|
|
10
|
+
/** (블록 안 줄 오프셋, 항목 전체). 이어지는 줄은 앞 항목에 붙인다. @param {string} text @returns {[number, string][]} */
|
|
11
|
+
function itemsOf(text) {
|
|
12
|
+
/** @type {[number, string][]} */
|
|
13
|
+
const items = [];
|
|
14
|
+
text.split("\n").forEach((line, offset) => {
|
|
15
|
+
if (BULLET.test(line)) {
|
|
16
|
+
items.push([offset, line.replace(BULLET, "").trim()]);
|
|
17
|
+
} else if (items.length && line.trim()) {
|
|
18
|
+
const last = items[items.length - 1];
|
|
19
|
+
last[1] = `${last[1]} ${line.trim()}`;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return items;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
27
|
+
* @param {import("../../config/settings.js").Config} config
|
|
28
|
+
*/
|
|
29
|
+
export function run(doc, config) {
|
|
30
|
+
if (doc.sections.length < 2) return [];
|
|
31
|
+
const last = doc.sections[doc.sections.length - 1];
|
|
32
|
+
if (last.isIntro) return [];
|
|
33
|
+
/** @type {[number, string][]} */
|
|
34
|
+
const over = [];
|
|
35
|
+
for (const block of doc.blocks) {
|
|
36
|
+
if (block.kind !== LIST || block.startLine < last.startLine) continue;
|
|
37
|
+
for (const [offset, item] of itemsOf(block.text)) {
|
|
38
|
+
const visible = plainText(item);
|
|
39
|
+
if (visible.length > config.moreLaterMaxChars) over.push([block.startLine + offset, visible]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!over.length) return [];
|
|
43
|
+
let best = over[0];
|
|
44
|
+
for (const pair of over) if (pair[1].length > best[1].length) best = pair;
|
|
45
|
+
return [
|
|
46
|
+
finding(name, best[0], best[1], `마지막 절의 목록 항목 ${over.length}개가 ${config.moreLaterMaxChars}자를 넘는다 (가장 긴 것 ${best[1].length}자). 미룬 것이 아니라 옮겨 적은 것이다. 한 줄과 링크로 줄인다`, null, NOTICE, SECTION, last.index),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
@@ -3,6 +3,7 @@ import { LIST, PROSE } from "../../document/model.js";
|
|
|
3
3
|
import { SECTION, finding } from "../finding.js";
|
|
4
4
|
|
|
5
5
|
export const name = "sectionNoProse";
|
|
6
|
+
export const mechanism = "threshold";
|
|
6
7
|
|
|
7
8
|
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
8
9
|
export function run(doc) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
import { CODE, IMAGE, TABLE } from "../../document/model.js";
|
|
2
|
+
import { CODE, EMBED, IMAGE, TABLE } from "../../document/model.js";
|
|
3
3
|
import { NOTICE, SECTION, finding } from "../finding.js";
|
|
4
4
|
|
|
5
5
|
export const name = "sectionResult";
|
|
6
|
-
const
|
|
6
|
+
export const mechanism = "threshold";
|
|
7
|
+
const RESULT_KINDS = [CODE, EMBED, IMAGE, TABLE];
|
|
7
8
|
const FILE_NAME = /[\w가-힣-]+\.(?:csv|xlsx|xls|parquet|json|png|jpg|svg|db|sqlite|txt|py|pdf|html|md)\b/;
|
|
8
9
|
const OUTPUT_WORDS = ["출력", "화면에", "찍힙니다", "나옵니다", "뜹니다", "보입니다", "만들어집니다", "생깁니다", "저장됩니다"];
|
|
9
10
|
|
package/src/text.js
CHANGED
|
@@ -61,3 +61,22 @@ export function countIn(text, needle, end = text.length) {
|
|
|
61
61
|
from = at + needle.length;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 파이썬 round(value, digits). 정확한 이진 값을 십진으로 풀어 반올림하고 정확히 절반이면 짝수로 간다.
|
|
67
|
+
* Math.round 는 절반을 위로 올려 13/16 을 0.813 으로 만들고 파이썬은 0.812 다. 두 판의 글자가 같으려면 파이썬 쪽이다.
|
|
68
|
+
* toFixed(100) 은 1e21 아래의 수를 정확한 십진 전개로 준다. 지문의 비율과 평균이 사는 범위다.
|
|
69
|
+
* @param {number} value
|
|
70
|
+
* @param {number} digits
|
|
71
|
+
*/
|
|
72
|
+
export function roundHalfEven(value, digits) {
|
|
73
|
+
if (!Number.isFinite(value) || Math.abs(value) >= 1e21) return value;
|
|
74
|
+
const sign = value < 0 ? -1 : 1;
|
|
75
|
+
const [whole, fraction] = Math.abs(value).toFixed(100).split(".");
|
|
76
|
+
const rest = fraction.slice(digits);
|
|
77
|
+
let units = BigInt(whole + fraction.slice(0, digits));
|
|
78
|
+
const next = rest.charCodeAt(0) - 48;
|
|
79
|
+
const halfway = next === 5 && /^0*$/.test(rest.slice(1));
|
|
80
|
+
if (next > 5 || (next === 5 && !halfway) || (halfway && units % 2n === 1n)) units += 1n;
|
|
81
|
+
return sign * (Number(units) / 10 ** digits);
|
|
82
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { DOCUMENT, NOTICE, finding } from "../finding.js";
|
|
3
|
-
|
|
4
|
-
export const name = "readerAbsent";
|
|
5
|
-
|
|
6
|
-
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
7
|
-
export function run(doc) {
|
|
8
|
-
if (doc.bodySections.length < 2 || doc.questionCount > 0 || doc.readerCallCount > 0) return [];
|
|
9
|
-
const first = doc.sentences.length ? doc.sentences[0] : null;
|
|
10
|
-
return [finding(name, first ? first.line : 1, first ? first.text : "", "질문도 독자를 부르는 문장도 없다. 독자가 할 행동을 동사로 끝내는 문장을 한 번은 넣는다", null, NOTICE, DOCUMENT, -1)];
|
|
11
|
-
}
|
package/src/rules/shared/runs.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
/** 연속 구간 찾기. 같은 값이 이어지는 자리를 [시작 위치, 길이, 값] 으로 준다. */
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @template T
|
|
6
|
-
* @param {T[]} values
|
|
7
|
-
* @param {number} minLength
|
|
8
|
-
* @returns {[number, number, T][]}
|
|
9
|
-
*/
|
|
10
|
-
export function runsOf(values, minLength) {
|
|
11
|
-
/** @type {[number, number, T][]} */
|
|
12
|
-
const runs = [];
|
|
13
|
-
let start = 0;
|
|
14
|
-
while (start < values.length) {
|
|
15
|
-
let end = start;
|
|
16
|
-
while (end + 1 < values.length && values[end + 1] === values[start]) end += 1;
|
|
17
|
-
const length = end - start + 1;
|
|
18
|
-
if (length >= minLength) runs.push([start, length, values[start]]);
|
|
19
|
-
start = end + 1;
|
|
20
|
-
}
|
|
21
|
-
return runs;
|
|
22
|
-
}
|