@salesforcedevs/sfdocs-liquid-lint-filtered-blocks 1.0.1-alpha.1 → 1.0.1-alpha.2
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/index.d.ts.map +1 -1
- package/dist/index.js +46 -61
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/index.test.ts +24 -77
- package/src/index.ts +58 -81
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA+HA,kBAA2C"}
|
package/dist/index.js
CHANGED
|
@@ -3,23 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
const unified_lint_rule_1 = __importDefault(require("unified-lint-rule"));
|
|
6
|
+
const unist_util_visit_1 = __importDefault(require("unist-util-visit"));
|
|
6
7
|
const sfdocs_liquid_lint_utils_1 = require("@salesforcedevs/sfdocs-liquid-lint-utils");
|
|
7
8
|
const SOURCE = 'sfdocs-liquid-lint:liquid-filtered-blocks';
|
|
8
9
|
function findBlockRanges(content, tagName) {
|
|
9
10
|
const ranges = [];
|
|
10
11
|
const tagRe = new RegExp(`{%-?\\s*(${tagName}|end${tagName})\\s*-?%}`, 'g');
|
|
11
|
-
const
|
|
12
|
+
const unmatchedOpeners = [];
|
|
12
13
|
let match;
|
|
13
14
|
while ((match = tagRe.exec(content)) !== null) {
|
|
14
15
|
if (match[1] === tagName) {
|
|
15
|
-
|
|
16
|
+
unmatchedOpeners.push(match.index);
|
|
16
17
|
}
|
|
17
|
-
else if (
|
|
18
|
-
const open = openStack.pop();
|
|
18
|
+
else if (unmatchedOpeners.length > 0) {
|
|
19
19
|
ranges.push({
|
|
20
|
-
openStart:
|
|
21
|
-
openEnd: open.end,
|
|
22
|
-
closeStart: match.index,
|
|
20
|
+
openStart: unmatchedOpeners.pop(),
|
|
23
21
|
closeEnd: match.index + match[0].length,
|
|
24
22
|
});
|
|
25
23
|
}
|
|
@@ -29,17 +27,8 @@ function findBlockRanges(content, tagName) {
|
|
|
29
27
|
function isInsideRange(offset, ranges) {
|
|
30
28
|
return ranges.some(r => offset >= r.openStart && offset < r.closeEnd);
|
|
31
29
|
}
|
|
32
|
-
function isNestedBlock(
|
|
33
|
-
|
|
34
|
-
for (let i = 0; i < ranges.length; i++) {
|
|
35
|
-
if (i === blockIndex)
|
|
36
|
-
continue;
|
|
37
|
-
const outer = ranges[i];
|
|
38
|
-
if (block.openStart > outer.openStart && block.closeEnd < outer.closeEnd) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return false;
|
|
30
|
+
function isNestedBlock(block, ranges) {
|
|
31
|
+
return ranges.some(r => r !== block && block.openStart > r.openStart && block.closeEnd < r.closeEnd);
|
|
43
32
|
}
|
|
44
33
|
function isEntireFileWrapped(content, ranges) {
|
|
45
34
|
if (ranges.length !== 1)
|
|
@@ -49,30 +38,27 @@ function isEntireFileWrapped(content, ranges) {
|
|
|
49
38
|
const after = content.slice(block.closeEnd);
|
|
50
39
|
return !/\S/.test(before) && !/\S/.test(after);
|
|
51
40
|
}
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
41
|
+
function findEmptyHeadings(tree, content, ranges) {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
const headings = [];
|
|
44
|
+
(0, unist_util_visit_1.default)(tree, 'heading', (node) => {
|
|
45
|
+
var _a, _b;
|
|
46
|
+
const offset = (_b = (_a = node.position) === null || _a === void 0 ? void 0 : _a.start) === null || _b === void 0 ? void 0 : _b.offset;
|
|
47
|
+
if (offset !== undefined && !isInsideRange(offset, ranges)) {
|
|
48
|
+
headings.push(node);
|
|
59
49
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
: content.length;
|
|
68
|
-
const headingLine = content.slice(sectionStart, content.indexOf('\n', sectionStart));
|
|
69
|
-
const afterHeading = content.slice(sectionStart + headingLine.length, sectionEnd);
|
|
70
|
-
const stripped = stripFilteredBlocks(afterHeading, ranges, sectionStart + headingLine.length);
|
|
50
|
+
});
|
|
51
|
+
const emptyHeadings = [];
|
|
52
|
+
for (let i = 0; i < headings.length; i++) {
|
|
53
|
+
const sectionStart = headings[i].position.end.offset;
|
|
54
|
+
const sectionEnd = (_b = (_a = headings[i + 1]) === null || _a === void 0 ? void 0 : _a.position.start.offset) !== null && _b !== void 0 ? _b : content.length;
|
|
55
|
+
const body = content.slice(sectionStart, sectionEnd);
|
|
56
|
+
const stripped = stripFilteredBlocks(body, ranges, sectionStart);
|
|
71
57
|
if (!/\S/.test(stripped)) {
|
|
72
|
-
|
|
58
|
+
emptyHeadings.push(headings[i]);
|
|
73
59
|
}
|
|
74
60
|
}
|
|
75
|
-
return
|
|
61
|
+
return emptyHeadings;
|
|
76
62
|
}
|
|
77
63
|
function stripFilteredBlocks(section, allRanges, sectionOffset) {
|
|
78
64
|
let result = section;
|
|
@@ -86,36 +72,35 @@ function stripFilteredBlocks(section, allRanges, sectionOffset) {
|
|
|
86
72
|
}
|
|
87
73
|
return result;
|
|
88
74
|
}
|
|
89
|
-
function
|
|
75
|
+
function validateTag(tree, file, raw, content, tagName) {
|
|
76
|
+
const ranges = findBlockRanges(content, tagName);
|
|
77
|
+
if (ranges.length === 0)
|
|
78
|
+
return;
|
|
79
|
+
for (const range of ranges) {
|
|
80
|
+
if (isNestedBlock(range, ranges)) {
|
|
81
|
+
file.message(`Redundant nested ${tagName} block — already inside a ${tagName} block.`, (0, sfdocs_liquid_lint_utils_1.offsetToPosition)(raw, range.openStart));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (isEntireFileWrapped(content, ranges)) {
|
|
85
|
+
file.message(`All content is inside ${tagName} block — use \`filters: [${tagName}]\` in TOC instead.`, (0, sfdocs_liquid_lint_utils_1.offsetToPosition)(raw, ranges[0].openStart));
|
|
86
|
+
}
|
|
87
|
+
for (const heading of findEmptyHeadings(tree, content, ranges)) {
|
|
88
|
+
file.message(`Heading would have no content after ${tagName} block is removed.`, heading);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function checkFilteredBlocks(tree, file, option) {
|
|
90
92
|
var _a;
|
|
91
|
-
|
|
93
|
+
const filterTags = option === null || option === void 0 ? void 0 : option.filterTags;
|
|
94
|
+
if (!file.path || !filterTags || filterTags.length === 0)
|
|
92
95
|
return;
|
|
93
96
|
const raw = String((_a = file.contents) !== null && _a !== void 0 ? _a : '');
|
|
94
97
|
if (!raw)
|
|
95
98
|
return;
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
99
|
+
const parsed = (0, sfdocs_liquid_lint_utils_1.parseFrontmatter)(raw);
|
|
100
|
+
if (!parsed)
|
|
98
101
|
return;
|
|
99
|
-
const content = (0, sfdocs_liquid_lint_utils_1.protectCodeBlocks)(raw);
|
|
100
102
|
for (const tagName of filterTags) {
|
|
101
|
-
|
|
102
|
-
if (ranges.length === 0)
|
|
103
|
-
continue;
|
|
104
|
-
for (let i = 0; i < ranges.length; i++) {
|
|
105
|
-
if (isNestedBlock(i, ranges)) {
|
|
106
|
-
const position = (0, sfdocs_liquid_lint_utils_1.offsetToPosition)(raw, ranges[i].openStart);
|
|
107
|
-
file.message(`Redundant nested ${tagName} block — already inside a ${tagName} block.`, position);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (isEntireFileWrapped(content, ranges)) {
|
|
111
|
-
const position = (0, sfdocs_liquid_lint_utils_1.offsetToPosition)(raw, ranges[0].openStart);
|
|
112
|
-
file.message(`All content is inside ${tagName} block — use \`filters: [${tagName}]\` in TOC instead.`, position);
|
|
113
|
-
}
|
|
114
|
-
const emptyHeadingOffsets = findEmptySections(content, ranges);
|
|
115
|
-
for (const offset of emptyHeadingOffsets) {
|
|
116
|
-
const position = (0, sfdocs_liquid_lint_utils_1.offsetToPosition)(raw, offset);
|
|
117
|
-
file.message(`Heading would have no content after ${tagName} block is removed.`, position);
|
|
118
|
-
}
|
|
103
|
+
validateTag(tree, file, raw, parsed.content, tagName);
|
|
119
104
|
}
|
|
120
105
|
}
|
|
121
106
|
module.exports = (0, unified_lint_rule_1.default)(SOURCE, checkFilteredBlocks);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;AAAA,0EAAqC;AAErC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforcedevs/sfdocs-liquid-lint-filtered-blocks",
|
|
3
|
-
"version": "1.0.1-alpha.
|
|
3
|
+
"version": "1.0.1-alpha.2",
|
|
4
4
|
"description": "Validates structural issues in liquid filter blocks (englishonly, future, etc.).",
|
|
5
5
|
"author": "SFDocs Team",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
},
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@salesforcedevs/sfdocs-liquid-lint-utils": "1.0.2-alpha.
|
|
17
|
+
"@salesforcedevs/sfdocs-liquid-lint-utils": "1.0.2-alpha.2",
|
|
18
18
|
"unified-lint-rule": "^1.0.6",
|
|
19
|
+
"unist-util-visit": "^2.0.3",
|
|
19
20
|
"vfile": "^4.2.1"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import remark from 'remark';
|
|
3
|
+
import frontmatter from 'remark-frontmatter';
|
|
3
4
|
import unified from 'unified';
|
|
4
5
|
import vfile, { VFile } from 'vfile';
|
|
5
6
|
|
|
@@ -9,6 +10,7 @@ const PAGE_PATH = '/repo/content/en-us/project/guides/page.md';
|
|
|
9
10
|
|
|
10
11
|
const run = (md: string, options?: any, p = PAGE_PATH): VFile =>
|
|
11
12
|
remark()
|
|
13
|
+
.use(frontmatter)
|
|
12
14
|
.use(plugin as unified.Attacher, options)
|
|
13
15
|
.processSync(vfile({ path: p, contents: md }));
|
|
14
16
|
|
|
@@ -46,21 +48,6 @@ describe('sfdocs-lint:liquid-filtered-blocks', () => {
|
|
|
46
48
|
expect(run(md, { filterTags: ['englishonly'] }).messages).toHaveLength(0);
|
|
47
49
|
});
|
|
48
50
|
|
|
49
|
-
test('no messages for valid future block', () => {
|
|
50
|
-
const md = dedent`
|
|
51
|
-
# Heading
|
|
52
|
-
|
|
53
|
-
Regular content.
|
|
54
|
-
|
|
55
|
-
{% future %}
|
|
56
|
-
Future content here.
|
|
57
|
-
{% endfuture %}
|
|
58
|
-
|
|
59
|
-
More content after.
|
|
60
|
-
`;
|
|
61
|
-
expect(run(md, { filterTags: ['future'] }).messages).toHaveLength(0);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
51
|
test('warns on nested same-tag blocks (englishonly)', () => {
|
|
65
52
|
const md = dedent`
|
|
66
53
|
{% englishonly %}
|
|
@@ -81,66 +68,26 @@ describe('sfdocs-lint:liquid-filtered-blocks', () => {
|
|
|
81
68
|
expect(messages[0].ruleId).toBe('liquid-filtered-blocks');
|
|
82
69
|
});
|
|
83
70
|
|
|
84
|
-
test('warns
|
|
71
|
+
test('warns when entire file is wrapped', () => {
|
|
85
72
|
const md = dedent`
|
|
86
|
-
{%
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{% future %}
|
|
90
|
-
Inner.
|
|
91
|
-
{% endfuture %}
|
|
73
|
+
{% englishonly %}
|
|
74
|
+
# Full Content
|
|
92
75
|
|
|
93
|
-
|
|
76
|
+
Everything here.
|
|
77
|
+
{% endenglishonly %}
|
|
94
78
|
`;
|
|
95
|
-
const messages = run(md, { filterTags: ['
|
|
79
|
+
const messages = run(md, { filterTags: ['englishonly'] }).messages;
|
|
96
80
|
expect(messages).toHaveLength(1);
|
|
97
81
|
expect(messages[0].message).toBe(
|
|
98
|
-
'
|
|
82
|
+
'All content is inside englishonly block — use `filters: [englishonly]` in TOC instead.',
|
|
99
83
|
);
|
|
100
84
|
});
|
|
101
85
|
|
|
102
|
-
test('
|
|
103
|
-
const md = dedent`
|
|
104
|
-
# Heading
|
|
105
|
-
|
|
106
|
-
Regular content.
|
|
107
|
-
|
|
108
|
-
{% future %}
|
|
109
|
-
Future content.
|
|
110
|
-
|
|
111
|
-
{% englishonly %}
|
|
112
|
-
English-only inside future.
|
|
113
|
-
{% endenglishonly %}
|
|
114
|
-
|
|
115
|
-
{% endfuture %}
|
|
116
|
-
|
|
117
|
-
More content.
|
|
118
|
-
`;
|
|
119
|
-
expect(run(md, { filterTags: ['englishonly', 'future'] }).messages).toHaveLength(0);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('does NOT warn on cross-tag nesting (future inside englishonly)', () => {
|
|
123
|
-
const md = dedent`
|
|
124
|
-
# Heading
|
|
125
|
-
|
|
126
|
-
Regular content.
|
|
127
|
-
|
|
128
|
-
{% englishonly %}
|
|
129
|
-
English content.
|
|
130
|
-
|
|
131
|
-
{% future %}
|
|
132
|
-
Future inside english.
|
|
133
|
-
{% endfuture %}
|
|
134
|
-
|
|
135
|
-
{% endenglishonly %}
|
|
136
|
-
|
|
137
|
-
More content.
|
|
138
|
-
`;
|
|
139
|
-
expect(run(md, { filterTags: ['englishonly', 'future'] }).messages).toHaveLength(0);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('warns when entire file is wrapped', () => {
|
|
86
|
+
test('warns when entire file is wrapped even with frontmatter', () => {
|
|
143
87
|
const md = dedent`
|
|
88
|
+
---
|
|
89
|
+
title: My Topic
|
|
90
|
+
---
|
|
144
91
|
{% englishonly %}
|
|
145
92
|
# Full Content
|
|
146
93
|
|
|
@@ -162,18 +109,18 @@ describe('sfdocs-lint:liquid-filtered-blocks', () => {
|
|
|
162
109
|
|
|
163
110
|
## Empty Section
|
|
164
111
|
|
|
165
|
-
{%
|
|
112
|
+
{% englishonly %}
|
|
166
113
|
This is the only content under this heading.
|
|
167
|
-
{%
|
|
114
|
+
{% endenglishonly %}
|
|
168
115
|
|
|
169
116
|
## Another Section
|
|
170
117
|
|
|
171
118
|
More regular content.
|
|
172
119
|
`;
|
|
173
|
-
const messages = run(md, { filterTags: ['
|
|
120
|
+
const messages = run(md, { filterTags: ['englishonly'] }).messages;
|
|
174
121
|
expect(messages).toHaveLength(1);
|
|
175
122
|
expect(messages[0].message).toBe(
|
|
176
|
-
'Heading would have no content after
|
|
123
|
+
'Heading would have no content after englishonly block is removed.',
|
|
177
124
|
);
|
|
178
125
|
});
|
|
179
126
|
|
|
@@ -192,15 +139,15 @@ describe('sfdocs-lint:liquid-filtered-blocks', () => {
|
|
|
192
139
|
|
|
193
140
|
test('handles whitespace-control syntax', () => {
|
|
194
141
|
const md = dedent`
|
|
195
|
-
{%-
|
|
196
|
-
{%-
|
|
142
|
+
{%- englishonly -%}
|
|
143
|
+
{%- englishonly -%}
|
|
197
144
|
Nested.
|
|
198
|
-
{%-
|
|
199
|
-
{%-
|
|
145
|
+
{%- endenglishonly -%}
|
|
146
|
+
{%- endenglishonly -%}
|
|
200
147
|
`;
|
|
201
|
-
const messages = run(md, { filterTags: ['
|
|
148
|
+
const messages = run(md, { filterTags: ['englishonly'] }).messages;
|
|
202
149
|
expect(messages).toHaveLength(1);
|
|
203
|
-
expect(messages[0].message).toContain('Redundant nested
|
|
150
|
+
expect(messages[0].message).toContain('Redundant nested englishonly block');
|
|
204
151
|
});
|
|
205
152
|
|
|
206
153
|
test('ignores tags inside fenced code blocks', () => {
|
|
@@ -228,7 +175,7 @@ describe('sfdocs-lint:liquid-filtered-blocks', () => {
|
|
|
228
175
|
Content.
|
|
229
176
|
{% endenglishonly %}
|
|
230
177
|
`;
|
|
231
|
-
const messages = run(md, { filterTags: ['englishonly'
|
|
178
|
+
const messages = run(md, { filterTags: ['englishonly'] }).messages;
|
|
232
179
|
expect(messages).toHaveLength(1);
|
|
233
180
|
expect(messages[0].message).toContain('englishonly');
|
|
234
181
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,35 +1,27 @@
|
|
|
1
1
|
import rule from 'unified-lint-rule';
|
|
2
|
+
import visit from 'unist-util-visit';
|
|
2
3
|
import { VFile } from 'vfile';
|
|
3
|
-
import { offsetToPosition,
|
|
4
|
+
import { offsetToPosition, parseFrontmatter } from '@salesforcedevs/sfdocs-liquid-lint-utils';
|
|
4
5
|
|
|
5
6
|
const SOURCE = 'sfdocs-liquid-lint:liquid-filtered-blocks';
|
|
6
7
|
|
|
7
|
-
interface FilteredBlocksOptions {
|
|
8
|
-
filterTags?: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
8
|
interface BlockRange {
|
|
12
9
|
openStart: number;
|
|
13
|
-
openEnd: number;
|
|
14
|
-
closeStart: number;
|
|
15
10
|
closeEnd: number;
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
function findBlockRanges(content: string, tagName: string): BlockRange[] {
|
|
19
14
|
const ranges: BlockRange[] = [];
|
|
20
15
|
const tagRe = new RegExp(`{%-?\\s*(${tagName}|end${tagName})\\s*-?%}`, 'g');
|
|
21
|
-
const
|
|
16
|
+
const unmatchedOpeners: number[] = [];
|
|
22
17
|
|
|
23
18
|
let match: RegExpExecArray | null;
|
|
24
19
|
while ((match = tagRe.exec(content)) !== null) {
|
|
25
20
|
if (match[1] === tagName) {
|
|
26
|
-
|
|
27
|
-
} else if (
|
|
28
|
-
const open = openStack.pop()!;
|
|
21
|
+
unmatchedOpeners.push(match.index);
|
|
22
|
+
} else if (unmatchedOpeners.length > 0) {
|
|
29
23
|
ranges.push({
|
|
30
|
-
openStart:
|
|
31
|
-
openEnd: open.end,
|
|
32
|
-
closeStart: match.index,
|
|
24
|
+
openStart: unmatchedOpeners.pop()!,
|
|
33
25
|
closeEnd: match.index + match[0].length,
|
|
34
26
|
});
|
|
35
27
|
}
|
|
@@ -41,16 +33,8 @@ function isInsideRange(offset: number, ranges: BlockRange[]): boolean {
|
|
|
41
33
|
return ranges.some(r => offset >= r.openStart && offset < r.closeEnd);
|
|
42
34
|
}
|
|
43
35
|
|
|
44
|
-
function isNestedBlock(
|
|
45
|
-
|
|
46
|
-
for (let i = 0; i < ranges.length; i++) {
|
|
47
|
-
if (i === blockIndex) continue;
|
|
48
|
-
const outer = ranges[i];
|
|
49
|
-
if (block.openStart > outer.openStart && block.closeEnd < outer.closeEnd) {
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return false;
|
|
36
|
+
function isNestedBlock(block: BlockRange, ranges: BlockRange[]): boolean {
|
|
37
|
+
return ranges.some(r => r !== block && block.openStart > r.openStart && block.closeEnd < r.closeEnd);
|
|
54
38
|
}
|
|
55
39
|
|
|
56
40
|
function isEntireFileWrapped(content: string, ranges: BlockRange[]): boolean {
|
|
@@ -61,34 +45,27 @@ function isEntireFileWrapped(content: string, ranges: BlockRange[]): boolean {
|
|
|
61
45
|
return !/\S/.test(before) && !/\S/.test(after);
|
|
62
46
|
}
|
|
63
47
|
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (/^#{1,6}\s+\S/.test(line) && !isInsideRange(offset, ranges)) {
|
|
71
|
-
headingPositions.push(offset);
|
|
48
|
+
function findEmptyHeadings(tree: any, content: string, ranges: BlockRange[]): any[] {
|
|
49
|
+
const headings: any[] = [];
|
|
50
|
+
visit(tree, 'heading', (node: any) => {
|
|
51
|
+
const offset = node.position?.start?.offset;
|
|
52
|
+
if (offset !== undefined && !isInsideRange(offset, ranges)) {
|
|
53
|
+
headings.push(node);
|
|
72
54
|
}
|
|
73
|
-
|
|
74
|
-
}
|
|
55
|
+
});
|
|
75
56
|
|
|
76
|
-
const
|
|
77
|
-
for (let i = 0; i <
|
|
78
|
-
const sectionStart =
|
|
79
|
-
const sectionEnd = i + 1
|
|
80
|
-
|
|
81
|
-
: content.length;
|
|
57
|
+
const emptyHeadings: any[] = [];
|
|
58
|
+
for (let i = 0; i < headings.length; i++) {
|
|
59
|
+
const sectionStart = headings[i].position.end.offset;
|
|
60
|
+
const sectionEnd = headings[i + 1]?.position.start.offset ?? content.length;
|
|
61
|
+
const body = content.slice(sectionStart, sectionEnd);
|
|
82
62
|
|
|
83
|
-
const
|
|
84
|
-
const afterHeading = content.slice(sectionStart + headingLine.length, sectionEnd);
|
|
85
|
-
|
|
86
|
-
const stripped = stripFilteredBlocks(afterHeading, ranges, sectionStart + headingLine.length);
|
|
63
|
+
const stripped = stripFilteredBlocks(body, ranges, sectionStart);
|
|
87
64
|
if (!/\S/.test(stripped)) {
|
|
88
|
-
|
|
65
|
+
emptyHeadings.push(headings[i]);
|
|
89
66
|
}
|
|
90
67
|
}
|
|
91
|
-
return
|
|
68
|
+
return emptyHeadings;
|
|
92
69
|
}
|
|
93
70
|
|
|
94
71
|
function stripFilteredBlocks(section: string, allRanges: BlockRange[], sectionOffset: number): string {
|
|
@@ -105,46 +82,46 @@ function stripFilteredBlocks(section: string, allRanges: BlockRange[], sectionOf
|
|
|
105
82
|
return result;
|
|
106
83
|
}
|
|
107
84
|
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!raw) return;
|
|
112
|
-
|
|
113
|
-
const filterTags = option?.filterTags;
|
|
114
|
-
if (!filterTags || filterTags.length === 0) return;
|
|
115
|
-
|
|
116
|
-
const content = protectCodeBlocks(raw);
|
|
85
|
+
function validateTag(tree: any, file: VFile, raw: string, content: string, tagName: string): void {
|
|
86
|
+
const ranges = findBlockRanges(content, tagName);
|
|
87
|
+
if (ranges.length === 0) return;
|
|
117
88
|
|
|
118
|
-
for (const
|
|
119
|
-
|
|
120
|
-
if (ranges.length === 0) continue;
|
|
121
|
-
|
|
122
|
-
for (let i = 0; i < ranges.length; i++) {
|
|
123
|
-
if (isNestedBlock(i, ranges)) {
|
|
124
|
-
const position = offsetToPosition(raw, ranges[i].openStart);
|
|
125
|
-
file.message(
|
|
126
|
-
`Redundant nested ${tagName} block — already inside a ${tagName} block.`,
|
|
127
|
-
position,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (isEntireFileWrapped(content, ranges)) {
|
|
133
|
-
const position = offsetToPosition(raw, ranges[0].openStart);
|
|
89
|
+
for (const range of ranges) {
|
|
90
|
+
if (isNestedBlock(range, ranges)) {
|
|
134
91
|
file.message(
|
|
135
|
-
`
|
|
136
|
-
|
|
92
|
+
`Redundant nested ${tagName} block — already inside a ${tagName} block.`,
|
|
93
|
+
offsetToPosition(raw, range.openStart),
|
|
137
94
|
);
|
|
138
95
|
}
|
|
96
|
+
}
|
|
139
97
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
98
|
+
if (isEntireFileWrapped(content, ranges)) {
|
|
99
|
+
file.message(
|
|
100
|
+
`All content is inside ${tagName} block — use \`filters: [${tagName}]\` in TOC instead.`,
|
|
101
|
+
offsetToPosition(raw, ranges[0].openStart),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const heading of findEmptyHeadings(tree, content, ranges)) {
|
|
106
|
+
file.message(
|
|
107
|
+
`Heading would have no content after ${tagName} block is removed.`,
|
|
108
|
+
heading,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function checkFilteredBlocks(tree: any, file: VFile, option?: { filterTags?: string[] }): void {
|
|
114
|
+
const filterTags = option?.filterTags;
|
|
115
|
+
if (!file.path || !filterTags || filterTags.length === 0) return;
|
|
116
|
+
|
|
117
|
+
const raw = String(file.contents ?? '');
|
|
118
|
+
if (!raw) return;
|
|
119
|
+
|
|
120
|
+
const parsed = parseFrontmatter(raw);
|
|
121
|
+
if (!parsed) return;
|
|
122
|
+
|
|
123
|
+
for (const tagName of filterTags) {
|
|
124
|
+
validateTag(tree, file, raw, parsed.content, tagName);
|
|
148
125
|
}
|
|
149
126
|
}
|
|
150
127
|
|