book-kit 0.0.0-stage → 0.6.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/LICENSE +21 -0
- package/NOTICE.md +72 -0
- package/README.md +251 -2
- package/bin/book-kit.js +115 -0
- package/commands/_emoji_io.py +32 -0
- package/commands/audit-chapter-lengths.js +244 -0
- package/commands/check-epub-size.js +68 -0
- package/commands/check-pdf-page-count.js +162 -0
- package/commands/composite-kindle-emoji.py +85 -0
- package/commands/composite-print-emoji.py +98 -0
- package/commands/conventions.js +76 -0
- package/commands/convert-pdfx.sh +194 -0
- package/commands/download-icc-profile.sh +105 -0
- package/commands/fonts.py +212 -0
- package/commands/generate-reference-docx.py +344 -0
- package/commands/kindle-fonts.js +173 -0
- package/commands/links-to-footnotes.js +366 -0
- package/commands/markdownlint.js +25 -0
- package/commands/minify-css.js +43 -0
- package/commands/optimize-images.js +73 -0
- package/commands/parity.sh +150 -0
- package/commands/postprocess-epub.js +541 -0
- package/commands/prep-print-src.js +77 -0
- package/commands/prose.js +38 -0
- package/commands/release-files.js +89 -0
- package/commands/reorder-print-frontmatter.js +79 -0
- package/commands/strip-page-rules.js +1168 -0
- package/commands/strip-property-rules.js +93 -0
- package/commands/update-key-takeaways.js +314 -0
- package/commands/validate.js +283 -0
- package/configs/markdownlint-cli2.jsonc +98 -0
- package/configs/textlintrc.json +22 -0
- package/css/theme/01-tokens.css +83 -0
- package/css/theme/02-fonts.css +79 -0
- package/css/theme/03-prose.css +53 -0
- package/css/theme/04-base.css +226 -0
- package/css/theme/05-components.css +546 -0
- package/css/theme/06-utilities.css +40 -0
- package/css/theme/07-ereader.css +245 -0
- package/css/theme/08-epub.css +865 -0
- package/css/theme/09-back-matter.css +47 -0
- package/css/theme/10-dark.css +302 -0
- package/css/theme/11-print.css +1229 -0
- package/css/theme.css +38 -0
- package/defaults/book.config.yml +318 -0
- package/defaults/pandoc-base.yml +19 -0
- package/defaults/pandoc-docx.yml +6 -0
- package/defaults/pandoc-epub-fonts.yml +6 -0
- package/defaults/pandoc-epub.yml +15 -0
- package/defaults/pandoc-kindle.yml +6 -0
- package/emoji/1f382.png +0 -0
- package/emoji/1f389.png +0 -0
- package/emoji/1f440.png +0 -0
- package/emoji/1f44d.png +0 -0
- package/emoji/1f44f.png +0 -0
- package/emoji/1f454.png +0 -0
- package/emoji/1f4a1.png +0 -0
- package/emoji/1f4bb.png +0 -0
- package/emoji/1f602.png +0 -0
- package/emoji/1f603.png +0 -0
- package/emoji/1f604.png +0 -0
- package/emoji/1f60a.png +0 -0
- package/emoji/1f60f.png +0 -0
- package/emoji/1f64c.png +0 -0
- package/emoji/1f64f.png +0 -0
- package/emoji/1f914.png +0 -0
- package/emoji/1f923.png +0 -0
- package/emoji/2714.png +0 -0
- package/emoji/274c.png +0 -0
- package/filters/add-div-titles.lua +196 -0
- package/filters/arc-notice.lua +36 -0
- package/filters/body-emoji-images.lua +48 -0
- package/filters/callout-classes.lua +40 -0
- package/filters/lib/book_kit.lua +110 -0
- package/filters/revision.lua +17 -0
- package/filters/strip-comments.lua +40 -0
- package/filters/strip-emoji-kindle.lua +134 -0
- package/fonts/LICENSE-source-code-pro.md +93 -0
- package/fonts/LICENSE-source-sans.md +93 -0
- package/fonts/LICENSE-source-serif.md +93 -0
- package/fonts/recipe.json +41 -0
- package/just/book-kit.just +229 -0
- package/lib/acronyms.js +47 -0
- package/lib/callouts.js +17 -0
- package/lib/config.js +211 -0
- package/lib/is-main.js +21 -0
- package/lib/lint.js +148 -0
- package/lib/pandoc.js +148 -0
- package/lib/paragraphs.js +173 -0
- package/lib/revision.js +35 -0
- package/lib/structure.js +39 -0
- package/lib/textlint-github-formatter.js +58 -0
- package/lib/validate.js +202 -0
- package/package.json +98 -4
- package/presets/business-nonfiction/CONVENTIONS.md +35 -0
- package/presets/business-nonfiction/preset.yml +128 -0
- package/profiles/pdfx-def.ps +42 -0
- package/requirements.txt +14 -0
- package/rules/no-ai-like-patterns.js +209 -0
- package/rules/no-assumptive.js +76 -0
- package/rules/no-bare-acronyms.js +129 -0
- package/rules/no-cliches.js +544 -0
- package/rules/no-cultural-idioms.js +422 -0
- package/rules/no-vague-intensifiers.js +94 -0
- package/templates/template-epub.html +79 -0
- package/templates/template.html +73 -0
- package/toolchain.json +42 -0
- package/validators/acronym-expansion.js +94 -0
- package/validators/bold-keywords.js +56 -0
- package/validators/callout-balance.js +74 -0
- package/validators/callout-divs.js +66 -0
- package/validators/callout-labels.js +102 -0
- package/validators/chapter-lengths.js +14 -0
- package/validators/chapter.js +57 -0
- package/validators/cross-references.js +45 -0
- package/validators/doubled-words.js +76 -0
- package/validators/duplicate-headings.js +52 -0
- package/validators/em-dashes.js +73 -0
- package/validators/escaped-smallcaps.js +46 -0
- package/validators/footnotes.js +78 -0
- package/validators/heading-hierarchy.js +58 -0
- package/validators/hypothetical-hooks.js +103 -0
- package/validators/index-integrity.js +28 -0
- package/validators/index.js +73 -0
- package/validators/list-punctuation.js +123 -0
- package/validators/min-opener-length.js +87 -0
- package/validators/no-bare-url-link-text.js +38 -0
- package/validators/no-todo-markers.js +49 -0
- package/validators/no-transition-todos.js +34 -0
- package/validators/number-range-dashes.js +72 -0
- package/validators/paragraph-length.js +62 -0
- package/validators/quote-punctuation.js +85 -0
- package/validators/section-intro-final-lead.js +90 -0
- package/validators/section-intro-has-lead.js +41 -0
- package/validators/section-intro-no-tldr.js +37 -0
- package/validators/section-intros.js +33 -0
- package/validators/sentence-starters.js +132 -0
- package/validators/smallcaps.js +67 -0
- package/validators/tldr-length.js +58 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* textlint rule: no-ai-like-patterns
|
|
3
|
+
*
|
|
4
|
+
* Detects formulaic writing patterns commonly associated with AI-generated text.
|
|
5
|
+
* Plain-language style guides favor specific, human-written prose over these.
|
|
6
|
+
*
|
|
7
|
+
* Catches:
|
|
8
|
+
* - Excessive hedging phrases ("It's important to note that...")
|
|
9
|
+
* - Formulaic transitions ("Furthermore", "Moreover", "Additionally", "That said")
|
|
10
|
+
* - Meta-commentary ("Let's explore...", "Let's dive into...")
|
|
11
|
+
* - Hollow summarization ("In conclusion", "To summarize", "Overall")
|
|
12
|
+
* - Setup phrases ("When it comes to...", "In terms of...")
|
|
13
|
+
* - Performative enthusiasm ("Great question!", "Excellent point!")
|
|
14
|
+
* - AI-characteristic vocabulary ("delve")
|
|
15
|
+
* - Formulaic rhetorical questions ("What does this mean?", "Why does this matter?")
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const PATTERNS = [
|
|
19
|
+
// === Hedging and qualifier phrases ===
|
|
20
|
+
{
|
|
21
|
+
pattern: /\bIt(?:'s| is) (?:important|critical|crucial|essential|notable|noteworthy) to (?:note|mention|remember|consider|point out|highlight|emphasize|stress|acknowledge|recognize|understand|realize|keep in mind) that\b/i,
|
|
22
|
+
message: 'Avoid hedging phrases like "It\'s important to note that…" — state your point directly.'
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: /\bIt(?:'s| is) worth (?:noting|mentioning|remembering|considering|pointing out|highlighting|emphasizing|stressing) that\b/i,
|
|
26
|
+
message: 'Avoid hedging phrases like "It\'s worth noting that…" — state your point directly.'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /\bIt should be (?:noted|mentioned|emphasized|stressed|pointed out|highlighted|acknowledged|recognized|understood) that\b/i,
|
|
30
|
+
message: 'Avoid passive hedging like "It should be noted that…" — state your point directly.'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
pattern: /\b(?:as I recall|if I recall(?: correctly)?|as I remember it|if memory serves)\b/i,
|
|
34
|
+
message: 'Avoid memory-hedging like "as I recall" — state the recollection directly or cut it.'
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// === Formulaic transitions ===
|
|
38
|
+
{
|
|
39
|
+
pattern: /^Furthermore(?:,|\b)/m,
|
|
40
|
+
message: '"Furthermore" is a formulaic transition. Use natural transitions or restructure the sentence.'
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /^Moreover(?:,|\b)/m,
|
|
44
|
+
message: '"Moreover" is a formulaic transition. Use natural transitions or restructure the sentence.'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
pattern: /^Additionally(?:,|\b)/m,
|
|
48
|
+
message: '"Additionally" is a formulaic transition. Use natural transitions or restructure the sentence.'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /^In addition(?:,|\b)/m,
|
|
52
|
+
message: '"In addition" is a formulaic transition. Use natural transitions or restructure the sentence.'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
pattern: /^That being said(?:,|\b)/m,
|
|
56
|
+
message: '"That being said" is a formulaic transition. Use natural transitions or restructure the sentence.'
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// === Meta-commentary (sentence-initial only) ===
|
|
60
|
+
{
|
|
61
|
+
pattern: /(?:^|(?<=\. ))Let(?:'s| us) (?:explore|dive into|take a (?:look|closer look|deep dive|deeper look) at|examine|consider|unpack|break down|delve into)\b/m,
|
|
62
|
+
message: 'Avoid meta-commentary like "Let\'s explore…" — just explore, dive in, or examine directly.'
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// === Hollow summarization ===
|
|
66
|
+
{
|
|
67
|
+
pattern: /^In conclusion(?:,|\b)/im,
|
|
68
|
+
message: '"In conclusion" is a hollow summarization phrase — the content should speak for itself.'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
pattern: /^To summarize(?:,|\b)/im,
|
|
72
|
+
message: '"To summarize" is a hollow summarization phrase — the content should speak for itself.'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
pattern: /^In summary(?:,|\b)/im,
|
|
76
|
+
message: '"In summary" is a hollow summarization phrase — the content should speak for itself.'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
pattern: /^Overall(?:,|\b)/im,
|
|
80
|
+
message: '"Overall" is a hollow summarization phrase — the content should speak for itself.'
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// === Setup phrases (sentence-initial only) ===
|
|
84
|
+
{
|
|
85
|
+
pattern: /(?:^|(?<=\. ))When it comes to\b/m,
|
|
86
|
+
message: '"When it comes to…" is a filler setup phrase — get to the point.'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
pattern: /^In terms of\b/im,
|
|
90
|
+
message: '"In terms of…" is a filler setup phrase — get to the point.'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
pattern: /^In the context of\b/im,
|
|
94
|
+
message: '"In the context of…" is a filler setup phrase — get to the point.'
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// === Performative enthusiasm ===
|
|
98
|
+
{
|
|
99
|
+
pattern: /(?:^|(?<=\. ))(?:Great|Excellent|Fantastic|Wonderful|Amazing|Brilliant) (?:question|point|observation|insight|idea|suggestion|example|approach)(?:[.!]|\b)/m,
|
|
100
|
+
message: 'Avoid performative enthusiasm like "Great question!" — maintain a measured, authentic tone.'
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// === AI-characteristic vocabulary ===
|
|
104
|
+
{
|
|
105
|
+
pattern: /\bdelve(?:s|d)?\b/i,
|
|
106
|
+
message: '"Delve" is an AI-characteristic word — use "examine", "explore", "look at", or "dig into" instead.'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
pattern: /(?:^|(?<=\. ))(?:So )?[Ww]hat does this (?:mean|look like)\?/m,
|
|
110
|
+
message: 'Avoid rhetorical questions like "What does this mean?" — state the point directly.'
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
pattern: /(?:^|(?<=\. ))Why does this matter\?/m,
|
|
114
|
+
message: 'Avoid rhetorical questions like "Why does this matter?" — state the point directly.'
|
|
115
|
+
},
|
|
116
|
+
// === Conversational-AI tics and reassurance ===
|
|
117
|
+
{
|
|
118
|
+
pattern: /\blet that sink in\b/i,
|
|
119
|
+
message: 'Avoid the conversational-AI tic "let that sink in" — trust the reader to absorb the point.'
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
pattern: /\bbuckle up\b/i,
|
|
123
|
+
message: 'Avoid the conversational-AI tic "buckle up" — get to the substance.'
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
pattern: /\bspoiler alert\b/i,
|
|
127
|
+
message: 'Avoid the conversational-AI tic "spoiler alert" — just state the point.'
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
pattern: /\bplot twist\b/i,
|
|
131
|
+
message: 'Avoid the conversational-AI tic "plot twist" — just state what happened.'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
pattern: /\bhere'?s the (?:kicker|catch)\b/i,
|
|
135
|
+
message: 'Avoid the conversational-AI tic "here\'s the kicker/catch" — state the consequence directly.'
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
pattern: /\blet'?s (?:face it|be honest)\b/i,
|
|
139
|
+
message: 'Avoid the conversational-AI tic "let\'s face it / let\'s be honest" — just make the claim.'
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
pattern: /\byou (?:might|may) be (?:wondering|asking yourself)\b/i,
|
|
143
|
+
message: 'Avoid "you might be wondering" — pose the question directly or cut the setup.'
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
pattern: /(?:—|–|, )and that'?s (?:okay|perfectly fine)\b/i,
|
|
147
|
+
message: 'Avoid the formulaic reassurance "and that\'s okay" — it reads as AI-generated padding.'
|
|
148
|
+
},
|
|
149
|
+
// === Filler openers ===
|
|
150
|
+
{
|
|
151
|
+
pattern: /(?:^|(?<=\. ))In today'?s\b/m,
|
|
152
|
+
message: '"In today\'s…" is a filler opener — get to the point.'
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
pattern: /(?:^|(?<=\. ))(?:In an era|In the (?:world|realm) of|Gone are the days)\b/m,
|
|
156
|
+
message: 'This is a filler opener — start with the substance.'
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
pattern: /(?:^|(?<=\. ))At its (?:core|heart)\b/m,
|
|
160
|
+
message: '"At its core/heart…" is an AI-characteristic essence framing — state the point directly.'
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
pattern: /(?:^|(?<=\. ))In essence\b/m,
|
|
164
|
+
message: '"In essence" is hollow framing — just state the essence.'
|
|
165
|
+
}
|
|
166
|
+
// NOTE: The negation-antithesis construction ("it's not X—it's Y"), "not only
|
|
167
|
+
// X but also Y", and the "that's where X comes in" bridge are deliberately NOT
|
|
168
|
+
// enforced here. Writers often use the device intentionally and well, and
|
|
169
|
+
// textlint has no suggestion tier — every match would fail `just prose`.
|
|
170
|
+
// Leave them to human review (or a suggestion-level Vale rule) instead.
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {import("@textlint/types").TextlintRuleContext} context
|
|
175
|
+
* @param {Object} options
|
|
176
|
+
* @param {string[]} [options.allow] - Patterns to skip (by message substring)
|
|
177
|
+
*/
|
|
178
|
+
function reporter(context, options = {}) {
|
|
179
|
+
const { Syntax, getSource, report, RuleError } = context;
|
|
180
|
+
const allow = options.allow || [];
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
[Syntax.Paragraph](node) {
|
|
184
|
+
// Strip inline code spans so quoted examples don't trigger rules
|
|
185
|
+
const text = getSource(node).replace(/`[^`]+`/g, match => ' '.repeat(match.length));
|
|
186
|
+
|
|
187
|
+
for (const { pattern, message } of PATTERNS) {
|
|
188
|
+
if (allow.some(a => message.includes(a))) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Reset regex state for global/multiline patterns
|
|
193
|
+
pattern.lastIndex = 0;
|
|
194
|
+
const match = pattern.exec(text);
|
|
195
|
+
|
|
196
|
+
if (match) {
|
|
197
|
+
const index = match.index;
|
|
198
|
+
report(node, new RuleError(message, { index }));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const rule = {
|
|
206
|
+
linter: reporter
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export default rule;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* textlint rule: no-assumptive
|
|
3
|
+
*
|
|
4
|
+
* Detects assumptive language that presupposes the reader already understands
|
|
5
|
+
* something, which condescends to readers who are less familiar with the topic.
|
|
6
|
+
* For a global, neurodiverse audience, clarity beats implied familiarity.
|
|
7
|
+
*
|
|
8
|
+
* Catches phrases like "simply", "of course", "as you know", "it's obvious that".
|
|
9
|
+
* Note: "needless to say" and "goes without saying" are caught by no-cliches.js.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const PATTERNS = [
|
|
13
|
+
{
|
|
14
|
+
pattern: /\bsimply\b/i,
|
|
15
|
+
message: '"Simply" assumes the reader finds this easy. If it\'s worth explaining, explain it without the qualifier.'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
pattern: /\bof course\b/i,
|
|
19
|
+
message: '"Of course" presupposes the reader already knows this. State it plainly or cut the phrase.'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
pattern: /\bas you(?:'re| are)? (?:probably |likely )?(?:already )?(?:aware|know|know,)\b/i,
|
|
23
|
+
message: '"As you know" assumes shared knowledge. State the point directly.'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
pattern: /\bas (?:we all|everyone) know(?:s)?\b/i,
|
|
27
|
+
message: '"As everyone knows" assumes universal knowledge. Not everyone does — state it plainly.'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
pattern: /\bit(?:'s| is) obvious(?: that)?\b/i,
|
|
31
|
+
message: '"It\'s obvious that" — if it were obvious, you wouldn\'t need to say it. Just say it.'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
pattern: /\bit(?:'s| is) clear(?: that)?\b/i,
|
|
35
|
+
message: '"It\'s clear that" implies the reader should already understand. State the point directly.'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
pattern: /\bas is (?:well |widely )?known\b/i,
|
|
39
|
+
message: '"As is well known" assumes shared awareness. State the point instead.'
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {import("@textlint/types").TextlintRuleContext} context
|
|
45
|
+
* @param {Object} options
|
|
46
|
+
* @param {string[]} [options.allow] - Patterns to skip (by message substring)
|
|
47
|
+
*/
|
|
48
|
+
function reporter(context, options = {}) {
|
|
49
|
+
const { Syntax, getSource, report, RuleError } = context;
|
|
50
|
+
const allow = options.allow || [];
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
[Syntax.Paragraph](node) {
|
|
54
|
+
const text = getSource(node);
|
|
55
|
+
|
|
56
|
+
for (const { pattern, message } of PATTERNS) {
|
|
57
|
+
if (allow.some(a => message.includes(a))) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pattern.lastIndex = 0;
|
|
62
|
+
const match = pattern.exec(text);
|
|
63
|
+
|
|
64
|
+
if (match) {
|
|
65
|
+
report(node, new RuleError(message, { index: match.index }));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const rule = {
|
|
73
|
+
linter: reporter
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default rule;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* textlint rule: no-bare-acronyms
|
|
3
|
+
*
|
|
4
|
+
* Detects acronyms not wrapped in `[ACRONYM]{.smallcaps}` formatting.
|
|
5
|
+
* In the business-nonfiction house style, acronyms from a known list (plus
|
|
6
|
+
* lint.acronyms) are written as pandoc smallcaps spans. For plurals, the "s" goes outside the span:
|
|
7
|
+
* `[API]{.smallcaps}s`
|
|
8
|
+
*
|
|
9
|
+
* Catches:
|
|
10
|
+
* - Bare acronyms: `API` → `[API]{.smallcaps}`
|
|
11
|
+
* - Bare plural acronyms: `APIs` → `[API]{.smallcaps}s`
|
|
12
|
+
*
|
|
13
|
+
* Skips acronyms inside:
|
|
14
|
+
* - Code spans (backticks)
|
|
15
|
+
* - Already-formatted smallcaps spans `[...]{.smallcaps}`
|
|
16
|
+
* - URLs and link targets
|
|
17
|
+
* - Pandoc div/span attributes `{.class ...}`
|
|
18
|
+
* - Image alt text `` and link definitions
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { smallcapsAcronyms } from '../lib/acronyms.js';
|
|
22
|
+
import { loadConfigOrDefaults } from '../lib/config.js';
|
|
23
|
+
|
|
24
|
+
// Known acronyms (base forms, no trailing "s"): the kit's list shared with
|
|
25
|
+
// `book-kit validate` (lib/acronyms.js) plus the book's lint.acronyms.
|
|
26
|
+
const BASE_ACRONYMS = new Set(smallcapsAcronyms(loadConfigOrDefaults()));
|
|
27
|
+
|
|
28
|
+
// Acronyms that can appear with a plural "s" suffix
|
|
29
|
+
const PLURALIZABLE = new Set([
|
|
30
|
+
'API', 'URL', 'PR', 'GIF', 'IC', 'DM', 'PM', 'VP', 'RFC', 'ADR', 'OKR'
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
34
|
+
|
|
35
|
+
// Build a single regex that matches any known acronym (optionally plural)
|
|
36
|
+
// sorted longest-first so e.g. HTTPS matches before HTTP
|
|
37
|
+
const sortedAcronyms = [...BASE_ACRONYMS].sort((a, b) => b.length - a.length);
|
|
38
|
+
const acronymPattern = new RegExp(
|
|
39
|
+
'(?<![\\[\\w])(' + sortedAcronyms.map(escapeRegExp).join('|') + ')(s)?(?![\\w\\]{}])',
|
|
40
|
+
'g'
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Replace regions that should be ignored with equal-length whitespace,
|
|
45
|
+
* preserving character offsets for accurate error reporting.
|
|
46
|
+
*/
|
|
47
|
+
function maskIgnoredRegions(text) {
|
|
48
|
+
const blank = (m) => ' '.repeat(m.length);
|
|
49
|
+
|
|
50
|
+
return text
|
|
51
|
+
// Already-formatted smallcaps: [ACRONYM]{.smallcaps} or [ACRONYM]{.smallcaps}s
|
|
52
|
+
.replace(/\[[^\]]*\]\{\.smallcaps\}s?/g, blank)
|
|
53
|
+
// Inline code spans
|
|
54
|
+
.replace(/`[^`]+`/g, blank)
|
|
55
|
+
// Image alt text and full image syntax: 
|
|
56
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, blank)
|
|
57
|
+
// Link targets (the URL part): [text](url) — blank only the (url)
|
|
58
|
+
.replace(/\]\([^)]*\)/g, blank)
|
|
59
|
+
// Autolinks: <http...>
|
|
60
|
+
.replace(/<https?:\/\/[^>]+>/g, blank)
|
|
61
|
+
// Bare URLs: http://... or https://...
|
|
62
|
+
.replace(/https?:\/\/[^\s)>]+/g, blank)
|
|
63
|
+
// Pandoc attributes: {.class ...} or {custom-style="..."}
|
|
64
|
+
.replace(/\{[^}]*\}/g, blank);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {import("@textlint/types").TextlintRuleContext} context
|
|
69
|
+
* @param {Object} options
|
|
70
|
+
* @param {string[]} [options.allow] - Acronyms to skip
|
|
71
|
+
*/
|
|
72
|
+
function reporter(context, options = {}) {
|
|
73
|
+
const { Syntax, getSource, report, RuleError, fixer } = context;
|
|
74
|
+
const allow = new Set(options.allow || []);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
[Syntax.Paragraph](node) {
|
|
78
|
+
const text = getSource(node);
|
|
79
|
+
const masked = maskIgnoredRegions(text);
|
|
80
|
+
|
|
81
|
+
acronymPattern.lastIndex = 0;
|
|
82
|
+
let match;
|
|
83
|
+
|
|
84
|
+
while ((match = acronymPattern.exec(masked)) !== null) {
|
|
85
|
+
const fullMatch = match[0];
|
|
86
|
+
const base = match[1];
|
|
87
|
+
const plural = match[2]; // "s" or undefined
|
|
88
|
+
|
|
89
|
+
// Skip allowed acronyms
|
|
90
|
+
if (allow.has(base)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If plural "s" found but this acronym isn't pluralizable, skip
|
|
95
|
+
// (could be a false positive)
|
|
96
|
+
if (plural && !PLURALIZABLE.has(base)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const index = match.index;
|
|
101
|
+
const replacement = plural
|
|
102
|
+
? `[${base}]{.smallcaps}s`
|
|
103
|
+
: `[${base}]{.smallcaps}`;
|
|
104
|
+
|
|
105
|
+
const message = plural
|
|
106
|
+
? `"${fullMatch}" should use smallcaps: ${replacement}`
|
|
107
|
+
: `"${base}" should use smallcaps: ${replacement}`;
|
|
108
|
+
|
|
109
|
+
report(
|
|
110
|
+
node,
|
|
111
|
+
new RuleError(message, {
|
|
112
|
+
index,
|
|
113
|
+
fix: fixer.replaceTextRange(
|
|
114
|
+
[index, index + fullMatch.length],
|
|
115
|
+
replacement
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const rule = {
|
|
125
|
+
linter: reporter,
|
|
126
|
+
fixer: reporter
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export default rule;
|