@khanacademy/perseus-linter 0.0.0-PR973-20240207213425 → 0.0.0-PR992-20240214202318

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.
Files changed (51) hide show
  1. package/package.json +7 -5
  2. package/.eslintrc.js +0 -12
  3. package/CHANGELOG.md +0 -168
  4. package/src/README.md +0 -41
  5. package/src/__tests__/matcher.test.ts +0 -498
  6. package/src/__tests__/rule.test.ts +0 -110
  7. package/src/__tests__/rules.test.ts +0 -548
  8. package/src/__tests__/selector-parser.test.ts +0 -51
  9. package/src/__tests__/tree-transformer.test.ts +0 -444
  10. package/src/index.ts +0 -281
  11. package/src/proptypes.ts +0 -19
  12. package/src/rule.ts +0 -419
  13. package/src/rules/absolute-url.ts +0 -23
  14. package/src/rules/all-rules.ts +0 -71
  15. package/src/rules/blockquoted-math.ts +0 -9
  16. package/src/rules/blockquoted-widget.ts +0 -9
  17. package/src/rules/double-spacing-after-terminal.ts +0 -11
  18. package/src/rules/extra-content-spacing.ts +0 -11
  19. package/src/rules/heading-level-1.ts +0 -13
  20. package/src/rules/heading-level-skip.ts +0 -19
  21. package/src/rules/heading-sentence-case.ts +0 -10
  22. package/src/rules/heading-title-case.ts +0 -68
  23. package/src/rules/image-alt-text.ts +0 -20
  24. package/src/rules/image-in-table.ts +0 -9
  25. package/src/rules/image-spaces-around-urls.ts +0 -34
  26. package/src/rules/image-widget.ts +0 -49
  27. package/src/rules/link-click-here.ts +0 -10
  28. package/src/rules/lint-utils.ts +0 -47
  29. package/src/rules/long-paragraph.ts +0 -13
  30. package/src/rules/math-adjacent.ts +0 -9
  31. package/src/rules/math-align-extra-break.ts +0 -10
  32. package/src/rules/math-align-linebreaks.ts +0 -42
  33. package/src/rules/math-empty.ts +0 -9
  34. package/src/rules/math-font-size.ts +0 -11
  35. package/src/rules/math-frac.ts +0 -9
  36. package/src/rules/math-nested.ts +0 -10
  37. package/src/rules/math-starts-with-space.ts +0 -11
  38. package/src/rules/math-text-empty.ts +0 -9
  39. package/src/rules/math-without-dollars.ts +0 -13
  40. package/src/rules/nested-lists.ts +0 -10
  41. package/src/rules/profanity.ts +0 -9
  42. package/src/rules/table-missing-cells.ts +0 -19
  43. package/src/rules/unbalanced-code-delimiters.ts +0 -13
  44. package/src/rules/unescaped-dollar.ts +0 -9
  45. package/src/rules/widget-in-table.ts +0 -9
  46. package/src/selector.ts +0 -504
  47. package/src/tree-transformer.ts +0 -583
  48. package/src/types.ts +0 -7
  49. package/src/version.ts +0 -10
  50. package/tsconfig-build.json +0 -12
  51. package/tsconfig-build.tsbuildinfo +0 -1
package/src/rule.ts DELETED
@@ -1,419 +0,0 @@
1
- /**
2
- * The Rule class represents a Perseus lint rule. A Rule instance has a check()
3
- * method that takes the same (node, state, content) arguments that a
4
- * TreeTransformer traversal callback function does. Call the check() method
5
- * during a tree traversal to determine whether the current node of the tree
6
- * violates the rule. If there is no violation, then check() returns
7
- * null. Otherwise, it returns an object that includes the name of the rule,
8
- * an error message, and the start and end positions within the node's content
9
- * string of the lint.
10
- *
11
- * A Perseus lint rule consists of a name, a severity, a selector, a pattern
12
- * (RegExp) and two functions. The check() method uses the selector, pattern,
13
- * and functions as follows:
14
- *
15
- * - First, when determining which rules to apply to a particular piece of
16
- * content, each rule can specify an optional function provided in the fifth
17
- * parameter to evaluate whether or not we should be applying this rule.
18
- * If the function returns false, we don't use the rule on this content.
19
- *
20
- * - Next, check() tests whether the node currently being traversed matches
21
- * the selector. If it does not, then the rule does not apply at this node
22
- * and there is no lint and check() returns null.
23
- *
24
- * - If the selector matched, then check() tests the text content of the node
25
- * (and its children) against the pattern. If the pattern does not match,
26
- * then there is no lint, and check() returns null.
27
- *
28
- * - If both the selector and pattern match, then check() calls the function
29
- * passing the TraversalState object, the content string for the node, the
30
- * array of nodes returned by the selector match, and the array of strings
31
- * returned by the pattern match. This function can use these arguments to
32
- * implement any kind of lint detection logic it wants. If it determines
33
- * that there is no lint, then it should return null. Otherwise, it should
34
- * return an error message as a string, or an object with `message`, `start`
35
- * and `end` properties. The start and end properties are numbers that mark
36
- * the beginning and end of the problematic content. Note that these numbers
37
- * are relative to the content string passed to the traversal callback, not
38
- * to the entire string that was used to generate the parse tree in the
39
- * first place. TODO(davidflanagan): modify the simple-markdown library to
40
- * have an option to add the text offset of each node to the parse
41
- * tree. This will allows us to pinpoint lint errors within a long string
42
- * of markdown text.
43
- *
44
- * - If the function returns null, then check() returns null. Otherwise,
45
- * check() returns an object with `rule`, `message`, `start` and `end`
46
- * properties. The value of the `rule` property is the name of the rule,
47
- * which is useful for error reporting purposes.
48
- *
49
- * The name, severity, selector, pattern and function arguments to the Rule()
50
- * constructor are optional, but you may not omit both the selector and the
51
- * pattern. If you do not specify a selector, a default selector that matches
52
- * any node of type "text" will be used. If you do not specify a pattern, then
53
- * any node that matches the selector will be assumed to match the pattern as
54
- * well. If you don't pass a function as the fourth argument to the Rule()
55
- * constructor, then you must pass an error message string instead. If you do
56
- * this, you'll get a default function that unconditionally returns an object
57
- * that includes the error message and the start and end indexes of the
58
- * portion of the content string that matched the pattern. If you don't pass a
59
- * function in the fifth parameter, the rule will be applied in any context.
60
- *
61
- * One of the design goals of this Rule class is to allow simple lint rules to
62
- * be described in JSON files without any JavaScript code. So in addition to
63
- * the Rule() constructor, the class also defines a Rule.makeRule() factory
64
- * method. This method takes a single object as its argument and expects the
65
- * object to have four string properties. The `name` property is passed as the
66
- * first argument to the Rule() construtctor. The optional `selector`
67
- * property, if specified, is passed to Selector.parse() and the resulting
68
- * Selector object is used as the second argument to Rule(). The optional
69
- * `pattern` property is converted to a RegExp before being passed as the
70
- * third argument to Rule(). (See Rule.makePattern() for details on the string
71
- * to RegExp conversion). Finally, the `message` property specifies an error
72
- * message that is passed as the final argument to Rule(). You can also use a
73
- * real RegExp as the value of the `pattern` property or define a custom lint
74
- * function on the `lint` property instead of setting the `message`
75
- * property. Doing either of these things means that your rule description can
76
- * no longer be saved in a JSON file, however.
77
- *
78
- * For example, here are two lint rules defined with Rule.makeRule():
79
- *
80
- * let nestedLists = Rule.makeRule({
81
- * name: "nested-lists",
82
- * selector: "list list",
83
- * message: `Nested lists:
84
- * nested lists are hard to read on mobile devices;
85
- * do not use additional indentation.`,
86
- * });
87
- *
88
- * let longParagraph = Rule.makeRule({
89
- * name: "long-paragraph",
90
- * selector: "paragraph",
91
- * pattern: /^.{501,}/,
92
- * lint: function(state, content, nodes, match) {
93
- * return `Paragraph too long:
94
- * This paragraph is ${content.length} characters long.
95
- * Shorten it to 500 characters or fewer.`;
96
- * },
97
- * });
98
- *
99
- * Certain advanced lint rules need additional information about the content
100
- * being linted in order to detect lint. For example, a rule to check for
101
- * whitespace at the start and end of the URL for an image can't use the
102
- * information in the node or content arguments because the markdown parser
103
- * strips leading and trailing whitespace when parsing. (Nevertheless, these
104
- * spaces have been a practical problem for our content translation process so
105
- * in order to check for them, a lint rule needs access to the original
106
- * unparsed source text. Similarly, there are various lint rules that check
107
- * widget usage. For example, it is easy to write a lint rule to ensure that
108
- * images have alt text for images encoded in markdown. But when images are
109
- * added to our content via an image widget we also want to be able to check
110
- * for alt text. In order to do this, the lint rule needs to be able to look
111
- * widgets up by name in the widgets object associated with the parse tree.
112
- *
113
- * In order to support advanced linting rules like these, the check() method
114
- * takes a context object as its optional fourth argument, and passes this
115
- * object on to the lint function of each rule. Rules that require extra
116
- * context should not assume that they will always get it, and should verify
117
- * that the necessary context has been supplied before using it. Currently the
118
- * "content" property of the context object is the unparsed source text if
119
- * available, and the "widgets" property of the context object is the widget
120
- * object associated with that content string in the JSON object that defines
121
- * the Perseus article or exercise that is being linted.
122
- */
123
-
124
- import {Errors, PerseusError} from "@khanacademy/perseus-error";
125
-
126
- import Selector from "./selector";
127
-
128
- import type {TraversalState, TreeNode} from "./tree-transformer";
129
-
130
- // This represents the type returned by String.match(). It is an
131
- // array of strings, but also has index:number and input:string properties.
132
- // TypeScript doesn't handle it well, so we punt and just use any.
133
- export type PatternMatchType = any;
134
-
135
- // This is the return type of the check() method of a Rule object
136
- export type RuleCheckReturnType =
137
- | {
138
- rule: string;
139
- message: string;
140
- start: number;
141
- end: number;
142
- severity?: number;
143
- }
144
- | null
145
- | undefined;
146
-
147
- // This is the return type of the lint detection function passed as the 4th
148
- // argument to the Rule() constructor. It can return null or a string or an
149
- // object containing a string and two numbers.
150
- // prettier-ignore
151
- // (prettier formats this in a way that ka-lint does not like)
152
- export type LintTesterReturnType = string | {
153
- message: string,
154
- start: number,
155
- end: number
156
- } | null | undefined;
157
-
158
- export type LintRuleContextObject = any | null | undefined;
159
-
160
- // This is the type of the lint detection function that the Rule() constructor
161
- // expects as its fourth argument. It is passed the TraversalState object and
162
- // content string that were passed to check(), and is also passed the array of
163
- // nodes returned by the selector match and the array of strings returned by
164
- // the pattern match. It should return null if no lint is detected or an
165
- // error message or an object contining an error message.
166
- export type LintTester = (
167
- state: TraversalState,
168
- content: string,
169
- selectorMatch: ReadonlyArray<TreeNode>,
170
- patternMatch: PatternMatchType,
171
- context: LintRuleContextObject,
172
- ) => LintTesterReturnType;
173
-
174
- // An optional check to verify whether or not a particular rule should
175
- // be checked by context. For example, some rules only apply in exercises,
176
- // and should never be applied to articles. Defaults to true, so if we
177
- // omit the applies function in a rule, it'll be tested everywhere.
178
- export type AppliesTester = (context: LintRuleContextObject) => boolean;
179
-
180
- /**
181
- * A Rule object describes a Perseus lint rule. See the comment at the top of
182
- * this file for detailed description.
183
- */
184
- export default class Rule {
185
- name: string; // The name of the rule
186
- severity: number; // The severity of the rule
187
- selector: Selector; // The specified selector or the DEFAULT_SELECTOR
188
- pattern: RegExp | null | undefined; // A regular expression if one was specified
189
- lint: LintTester; // The lint-testing function or a default
190
- applies: AppliesTester; // Checks to see if we should apply a rule or not
191
- message: string | null | undefined; // The error message for use with the default function
192
- static DEFAULT_SELECTOR: Selector;
193
-
194
- // The comment at the top of this file has detailed docs for
195
- // this constructor and its arguments
196
- constructor(
197
- name: string | null | undefined,
198
- severity: number | null | undefined,
199
- selector: Selector | null | undefined,
200
- pattern: RegExp | null | undefined,
201
- lint: LintTester | string,
202
- applies: AppliesTester,
203
- ) {
204
- if (!selector && !pattern) {
205
- throw new PerseusError(
206
- "Lint rules must have a selector or pattern",
207
- Errors.InvalidInput,
208
- {metadata: {name}},
209
- );
210
- }
211
-
212
- this.name = name || "unnamed rule";
213
- this.severity = severity || Rule.Severity.BULK_WARNING;
214
- this.selector = selector || Rule.DEFAULT_SELECTOR;
215
- this.pattern = pattern || null;
216
-
217
- // If we're called with an error message instead of a function then
218
- // use a default function that will return the message.
219
- if (typeof lint === "function") {
220
- this.lint = lint;
221
- this.message = null;
222
- } else {
223
- this.lint = (...args) => this._defaultLintFunction(...args);
224
- this.message = lint;
225
- }
226
-
227
- this.applies =
228
- applies ||
229
- function () {
230
- return true;
231
- };
232
- }
233
-
234
- // A factory method for use with rules described in JSON files
235
- // See the documentation at the start of this file for details.
236
- static makeRule(options: any): Rule {
237
- return new Rule(
238
- options.name,
239
- options.severity,
240
- options.selector ? Selector.parse(options.selector) : null,
241
- Rule.makePattern(options.pattern),
242
- options.lint || options.message,
243
- options.applies,
244
- );
245
- }
246
-
247
- // Check the node n to see if it violates this lint rule. A return value
248
- // of false means there is no lint. A returned object indicates a lint
249
- // error. See the documentation at the top of this file for details.
250
- check(
251
- node: TreeNode,
252
- traversalState: TraversalState,
253
- content: string,
254
- context: LintRuleContextObject,
255
- ): RuleCheckReturnType {
256
- // First, see if we match the selector.
257
- // If no selector was passed to the constructor, we use a
258
- // default selector that matches text nodes.
259
- const selectorMatch = this.selector.match(traversalState);
260
-
261
- // If the selector did not match, then we're done
262
- if (!selectorMatch) {
263
- return null;
264
- }
265
-
266
- // If the selector matched, then see if the pattern matches
267
- let patternMatch;
268
- if (this.pattern) {
269
- patternMatch = content.match(this.pattern);
270
- } else {
271
- // If there is no pattern, then just match all of the content.
272
- // Use a fake RegExp match object to represent this default match.
273
- patternMatch = Rule.FakePatternMatch(content, content, 0);
274
- }
275
-
276
- // If there was a pattern and it didn't match, then we're done
277
- if (!patternMatch) {
278
- return null;
279
- }
280
-
281
- try {
282
- // If we get here, then the selector and pattern have matched
283
- // so now we call the lint function to see if there is lint.
284
- const error = this.lint(
285
- traversalState,
286
- content,
287
- selectorMatch,
288
- patternMatch,
289
- context,
290
- );
291
-
292
- if (!error) {
293
- return null; // No lint; we're done
294
- }
295
- if (typeof error === "string") {
296
- // If the lint function returned a string we assume it
297
- // applies to the entire content of the node and return it.
298
- return {
299
- rule: this.name,
300
- severity: this.severity,
301
- message: error,
302
- start: 0,
303
- end: content.length,
304
- };
305
- }
306
- // If the lint function returned an object, then we just
307
- // add the rule name to the message, start and end.
308
- return {
309
- rule: this.name,
310
- severity: this.severity,
311
- message: error.message,
312
- start: error.start,
313
- end: error.end,
314
- };
315
- } catch (e: any) {
316
- // If the lint function threw an exception we handle that as
317
- // a special type of lint. We want the user to see the lint
318
- // warning in this case (even though it is out of their control)
319
- // so that the bug gets reported. Otherwise we'd never know that
320
- // a rule was failing.
321
- return {
322
- rule: "lint-rule-failure",
323
- message: `Exception in rule ${this.name}: ${e.message}
324
- Stack trace:
325
- ${e.stack}`,
326
- start: 0,
327
- end: content.length,
328
- };
329
- }
330
- }
331
-
332
- // This internal method is the default lint function that we use when a
333
- // rule is defined without a function. This is useful for rules where the
334
- // selector and/or pattern match are enough to indicate lint. This
335
- // function unconditionally returns the error message that was passed in
336
- // place of a function, but also adds start and end properties that
337
- // specify which particular portion of the node content matched the
338
- // pattern.
339
- _defaultLintFunction(
340
- state: TraversalState,
341
- content: string,
342
- selectorMatch: ReadonlyArray<TreeNode>,
343
- patternMatch: PatternMatchType,
344
- context: LintRuleContextObject,
345
- ): {
346
- end: number;
347
- message: string;
348
- start: number;
349
- } {
350
- return {
351
- message: this.message || "",
352
- start: patternMatch.index,
353
- end: patternMatch.index + patternMatch[0].length,
354
- };
355
- }
356
-
357
- // The makeRule() factory function uses this static method to turn its
358
- // argument into a RegExp. If the argument is already a RegExp, we just
359
- // return it. Otherwise, we compile it into a RegExp and return that.
360
- // The reason this is necessary is that Rule.makeRule() is designed for
361
- // use with data from JSON files and JSON files can't include RegExp
362
- // literals. Strings passed to this function do not need to be delimited
363
- // with / characters unless you want to include flags for the RegExp.
364
- //
365
- // Examples:
366
- //
367
- // input "" ==> output null
368
- // input /foo/ ==> output /foo/
369
- // input "foo" ==> output /foo/
370
- // input "/foo/i" ==> output /foo/i
371
- //
372
- static makePattern(
373
- pattern?: RegExp | string | null,
374
- ): RegExp | null | undefined {
375
- if (!pattern) {
376
- return null;
377
- }
378
- if (pattern instanceof RegExp) {
379
- return pattern;
380
- }
381
- if (pattern[0] === "/") {
382
- const lastSlash = pattern.lastIndexOf("/");
383
- const expression = pattern.substring(1, lastSlash);
384
- const flags = pattern.substring(lastSlash + 1);
385
- // @ts-expect-error - TS2713 - Cannot access 'RegExp.flags' because 'RegExp' is a type, but not a namespace. Did you mean to retrieve the type of the property 'flags' in 'RegExp' with 'RegExp["flags"]'?
386
- return new RegExp(expression, flags as RegExp.flags);
387
- }
388
- return new RegExp(pattern);
389
- }
390
-
391
- // This static method returns an string array with index and input
392
- // properties added, in order to simulate the return value of the
393
- // String.match() method. We use it when a Rule has no pattern and we
394
- // want to simulate a match on the entire content string.
395
- static FakePatternMatch(
396
- input: string,
397
- match: string | null | undefined,
398
- index: number,
399
- ): PatternMatchType {
400
- const result: any = [match];
401
- result.index = index;
402
- result.input = input;
403
- return result;
404
- }
405
-
406
- static Severity: {
407
- BULK_WARNING: number;
408
- ERROR: number;
409
- GUIDELINE: number;
410
- WARNING: number;
411
- } = {
412
- ERROR: 1,
413
- WARNING: 2,
414
- GUIDELINE: 3,
415
- BULK_WARNING: 4,
416
- };
417
- }
418
-
419
- Rule.DEFAULT_SELECTOR = Selector.parse("text");
@@ -1,23 +0,0 @@
1
- import Rule from "../rule";
2
-
3
- import {getHostname} from "./lint-utils";
4
-
5
- export default Rule.makeRule({
6
- name: "absolute-url",
7
- severity: Rule.Severity.GUIDELINE,
8
- selector: "link, image",
9
- lint: function (state, content, nodes, match) {
10
- const url = nodes[0].target;
11
- const hostname = getHostname(url);
12
-
13
- if (
14
- hostname === "khanacademy.org" ||
15
- hostname.endsWith(".khanacademy.org")
16
- ) {
17
- return `Don't use absolute URLs:
18
- When linking to KA content or images, omit the
19
- https://www.khanacademy.org URL prefix.
20
- Use a relative URL beginning with / instead.`;
21
- }
22
- },
23
- }) as Rule;
@@ -1,71 +0,0 @@
1
- // TODO(davidflanagan):
2
- // This should probably be converted to use import and to export
3
- // and object that maps rule names to rules. Also, maybe this should
4
- // be an auto-generated file with a script that updates it any time
5
- // we add a new rule?
6
-
7
- import AbsoluteUrl from "./absolute-url";
8
- import BlockquotedMath from "./blockquoted-math";
9
- import BlockquotedWidget from "./blockquoted-widget";
10
- import DoubleSpacingAfterTerminal from "./double-spacing-after-terminal";
11
- import ExtraContentSpacing from "./extra-content-spacing";
12
- import HeadingLevel1 from "./heading-level-1";
13
- import HeadingLevelSkip from "./heading-level-skip";
14
- import HeadingSentenceCase from "./heading-sentence-case";
15
- import HeadingTitleCase from "./heading-title-case";
16
- import ImageAltText from "./image-alt-text";
17
- import ImageInTable from "./image-in-table";
18
- import ImageSpacesAroundUrls from "./image-spaces-around-urls";
19
- import ImageWidget from "./image-widget";
20
- import LinkClickHere from "./link-click-here";
21
- import LongParagraph from "./long-paragraph";
22
- import MathAdjacent from "./math-adjacent";
23
- import MathAlignExtraBreak from "./math-align-extra-break";
24
- import MathAlignLinebreaks from "./math-align-linebreaks";
25
- import MathEmpty from "./math-empty";
26
- import MathFontSize from "./math-font-size";
27
- import MathFrac from "./math-frac";
28
- import MathNested from "./math-nested";
29
- import MathStartsWithSpace from "./math-starts-with-space";
30
- import MathTextEmpty from "./math-text-empty";
31
- import MathWithoutDollars from "./math-without-dollars";
32
- import NestedLists from "./nested-lists";
33
- import Profanity from "./profanity";
34
- import TableMissingCells from "./table-missing-cells";
35
- import UnbalancedCodeDelimiters from "./unbalanced-code-delimiters";
36
- import UnescapedDollar from "./unescaped-dollar";
37
- import WidgetInTable from "./widget-in-table";
38
-
39
- export default [
40
- AbsoluteUrl,
41
- BlockquotedMath,
42
- BlockquotedWidget,
43
- DoubleSpacingAfterTerminal,
44
- ExtraContentSpacing,
45
- HeadingLevel1,
46
- HeadingLevelSkip,
47
- HeadingSentenceCase,
48
- HeadingTitleCase,
49
- ImageAltText,
50
- ImageInTable,
51
- LinkClickHere,
52
- LongParagraph,
53
- MathAdjacent,
54
- MathAlignExtraBreak,
55
- MathAlignLinebreaks,
56
- MathEmpty,
57
- MathFontSize,
58
- MathFrac,
59
- MathNested,
60
- MathStartsWithSpace,
61
- MathTextEmpty,
62
- NestedLists,
63
- TableMissingCells,
64
- UnescapedDollar,
65
- WidgetInTable,
66
- Profanity,
67
- MathWithoutDollars,
68
- UnbalancedCodeDelimiters,
69
- ImageSpacesAroundUrls,
70
- ImageWidget,
71
- ];
@@ -1,9 +0,0 @@
1
- import Rule from "../rule";
2
-
3
- export default Rule.makeRule({
4
- name: "blockquoted-math",
5
- severity: Rule.Severity.WARNING,
6
- selector: "blockQuote math, blockQuote blockMath",
7
- message: `Blockquoted math:
8
- math should not be indented.`,
9
- }) as Rule;
@@ -1,9 +0,0 @@
1
- import Rule from "../rule";
2
-
3
- export default Rule.makeRule({
4
- name: "blockquoted-widget",
5
- severity: Rule.Severity.WARNING,
6
- selector: "blockQuote widget",
7
- message: `Blockquoted widget:
8
- widgets should not be indented.`,
9
- }) as Rule;
@@ -1,11 +0,0 @@
1
- /* eslint-disable no-useless-escape */
2
- import Rule from "../rule";
3
-
4
- export default Rule.makeRule({
5
- name: "double-spacing-after-terminal",
6
- severity: Rule.Severity.BULK_WARNING,
7
- selector: "paragraph",
8
- pattern: /[.!\?] {2}/i,
9
- message: `Use a single space after a sentence-ending period, or
10
- any other kind of terminal punctuation.`,
11
- }) as Rule;
@@ -1,11 +0,0 @@
1
- import Rule from "../rule";
2
-
3
- export default Rule.makeRule({
4
- name: "extra-content-spacing",
5
- selector: "paragraph",
6
- pattern: /\s+$/,
7
- applies: function (context) {
8
- return context.contentType === "article";
9
- },
10
- message: `No extra whitespace at the end of content blocks.`,
11
- }) as Rule;
@@ -1,13 +0,0 @@
1
- import Rule from "../rule";
2
-
3
- export default Rule.makeRule({
4
- name: "heading-level-1",
5
- severity: Rule.Severity.WARNING,
6
- selector: "heading",
7
- lint: function (state, content, nodes, match) {
8
- if (nodes[0].level === 1) {
9
- return `Don't use level-1 headings:
10
- Begin headings with two or more # characters.`;
11
- }
12
- },
13
- }) as Rule;
@@ -1,19 +0,0 @@
1
- import Rule from "../rule";
2
-
3
- export default Rule.makeRule({
4
- name: "heading-level-skip",
5
- severity: Rule.Severity.WARNING,
6
- selector: "heading ~ heading",
7
- lint: function (state, content, nodes, match) {
8
- const currentHeading = nodes[1];
9
- const previousHeading = nodes[0];
10
- // A heading can have a level less than, the same as
11
- // or one more than the previous heading. But going up
12
- // by 2 or more levels is not right
13
- if (currentHeading.level > previousHeading.level + 1) {
14
- return `Skipped heading level:
15
- this heading is level ${currentHeading.level} but
16
- the previous heading was level ${previousHeading.level}`;
17
- }
18
- },
19
- }) as Rule;
@@ -1,10 +0,0 @@
1
- import Rule from "../rule";
2
-
3
- export default Rule.makeRule({
4
- name: "heading-sentence-case",
5
- severity: Rule.Severity.GUIDELINE,
6
- selector: "heading",
7
- pattern: /^\W*[a-z]/, // first letter is lowercase
8
- message: `First letter is lowercase:
9
- the first letter of a heading should be capitalized.`,
10
- }) as Rule;
@@ -1,68 +0,0 @@
1
- import Rule from "../rule";
2
-
3
- // These are 3-letter and longer words that we would not expect to be
4
- // capitalized even in a title-case heading. See
5
- // http://blog.apastyle.org/apastyle/2012/03/title-case-and-sentence-case-capitalization-in-apa-style.html
6
- const littleWords = {
7
- and: true,
8
- nor: true,
9
- but: true,
10
- the: true,
11
- for: true,
12
- } as const;
13
-
14
- function isCapitalized(word: any) {
15
- const c = word[0];
16
- return c === c.toUpperCase();
17
- }
18
-
19
- export default Rule.makeRule({
20
- name: "heading-title-case",
21
- severity: Rule.Severity.GUIDELINE,
22
- selector: "heading",
23
- pattern: /[^\s:]\s+[A-Z]+[a-z]/,
24
- locale: "en",
25
- lint: function (state, content, nodes, match) {
26
- // We want to assert that heading text is in sentence case, not
27
- // title case. The pattern above requires a capital letter at the
28
- // start of the heading and allows them after a colon, or in
29
- // acronyms that are all capitalized.
30
- //
31
- // But we can't warn just because the pattern matched because
32
- // proper nouns are also allowed bo be capitalized. We're not
33
- // going to do dictionary lookup to check for proper nouns, so
34
- // we try a heuristic: if the title is more than 3 words long
35
- // and if all the words are capitalized or are on the list of
36
- // words that don't get capitalized, then we'll assume that
37
- // the heading is incorrectly in title case and will warn.
38
- // But if there is at least one non-capitalized long word then
39
- // we're not in title case and we should not warn.
40
- //
41
- // TODO(davidflanagan): if this rule causes a lot of false
42
- // positives, we should tweak it or remove it. Note that it will
43
- // fail for headings like "World War II in Russia"
44
- //
45
- // TODO(davidflanagan): This rule is specific to English.
46
- // It is marked with a locale property above, but that is NYI
47
- //
48
- // for APA style rules for title case
49
-
50
- const heading = content.trim();
51
- let words = heading.split(/\s+/);
52
-
53
- // Remove the first word and the little words
54
- words.shift();
55
- words = words.filter(
56
- // eslint-disable-next-line no-prototype-builtins
57
- (w) => w.length > 2 && !littleWords.hasOwnProperty(w),
58
- );
59
-
60
- // If there are at least 3 remaining words and all
61
- // are capitalized, then the heading is in title case.
62
- if (words.length >= 3 && words.every((w) => isCapitalized(w))) {
63
- return `Title-case heading:
64
- This heading appears to be in title-case, but should be sentence-case.
65
- Only capitalize the first letter and proper nouns.`;
66
- }
67
- },
68
- }) as Rule;