comment-parser 1.0.0 → 1.1.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/CHANGELOG.md +13 -0
- package/README.md +26 -19
- package/browser/index.js +331 -272
- package/es6/index.d.ts +21 -4
- package/es6/index.js +21 -5
- package/es6/parser/block-parser.d.ts +18 -2
- package/es6/parser/block-parser.js +12 -12
- package/es6/parser/index.d.ts +4 -4
- package/es6/parser/index.js +25 -26
- package/es6/parser/source-parser.js +13 -15
- package/es6/parser/spec-parser.d.ts +1 -6
- package/es6/parser/spec-parser.js +4 -151
- package/es6/parser/tokenizers/description.d.ts +19 -0
- package/es6/parser/tokenizers/description.js +46 -0
- package/es6/parser/tokenizers/index.d.ts +7 -0
- package/es6/parser/tokenizers/index.js +1 -0
- package/es6/parser/tokenizers/name.d.ts +6 -0
- package/es6/parser/tokenizers/name.js +91 -0
- package/es6/parser/tokenizers/tag.d.ts +6 -0
- package/es6/parser/tokenizers/tag.js +24 -0
- package/es6/parser/tokenizers/type.d.ts +27 -0
- package/es6/parser/tokenizers/type.js +67 -0
- package/es6/stringifier/index.d.ts +2 -1
- package/es6/stringifier/index.js +1 -6
- package/es6/stringifier/inspect.d.ts +2 -0
- package/es6/stringifier/inspect.js +41 -0
- package/es6/transforms/align.js +48 -43
- package/es6/transforms/indent.js +11 -22
- package/es6/transforms/index.d.ts +0 -2
- package/es6/transforms/index.js +2 -10
- package/es6/util.d.ts +11 -0
- package/es6/util.js +25 -25
- package/jest.config.js +12 -11
- package/lib/index.d.ts +21 -4
- package/lib/index.js +24 -6
- package/lib/parser/block-parser.d.ts +18 -2
- package/lib/parser/block-parser.js +12 -12
- package/lib/parser/index.d.ts +4 -4
- package/lib/parser/index.js +26 -27
- package/lib/parser/source-parser.js +14 -16
- package/lib/parser/spec-parser.d.ts +1 -6
- package/lib/parser/spec-parser.js +4 -156
- package/lib/parser/tokenizers/description.d.ts +19 -0
- package/lib/parser/tokenizers/description.js +51 -0
- package/lib/parser/tokenizers/index.d.ts +7 -0
- package/lib/parser/tokenizers/index.js +2 -0
- package/lib/parser/tokenizers/name.d.ts +6 -0
- package/lib/parser/tokenizers/name.js +94 -0
- package/lib/parser/tokenizers/tag.d.ts +6 -0
- package/lib/parser/tokenizers/tag.js +27 -0
- package/lib/parser/tokenizers/type.d.ts +27 -0
- package/lib/parser/tokenizers/type.js +70 -0
- package/lib/stringifier/index.d.ts +2 -1
- package/lib/stringifier/index.js +1 -6
- package/lib/stringifier/inspect.d.ts +2 -0
- package/lib/stringifier/inspect.js +44 -0
- package/lib/transforms/align.js +50 -45
- package/lib/transforms/indent.js +12 -23
- package/lib/transforms/index.d.ts +0 -2
- package/lib/transforms/index.js +3 -13
- package/lib/util.d.ts +11 -0
- package/lib/util.js +27 -26
- package/migrate-1.0.md +7 -7
- package/package.json +1 -1
- package/src/index.ts +20 -2
- package/src/parser/block-parser.ts +19 -1
- package/src/parser/index.ts +22 -21
- package/src/parser/source-parser.ts +2 -2
- package/src/parser/spec-parser.ts +2 -170
- package/src/parser/tokenizers/description.ts +75 -0
- package/src/parser/tokenizers/index.ts +8 -0
- package/src/parser/tokenizers/name.ts +112 -0
- package/src/parser/tokenizers/tag.ts +30 -0
- package/src/parser/tokenizers/type.ts +92 -0
- package/src/stringifier/index.ts +3 -1
- package/src/stringifier/inspect.ts +64 -0
- package/src/transforms/align.ts +37 -12
- package/src/transforms/index.ts +0 -3
- package/src/util.ts +20 -0
- package/tests/e2e/examples.js +39 -2
- package/tests/e2e/examples.spec.js +4 -2
- package/tests/e2e/issue-109.spec.js +49 -0
- package/tests/e2e/issue-112.spec.js +20 -0
- package/tests/e2e/issue-113.spec.js +23 -0
- package/tests/e2e/issue-119.spec.js +29 -0
- package/tests/e2e/issue-120.spec.js +29 -0
- package/tests/e2e/issue-121.spec.js +102 -0
- package/tests/e2e/transforms.spec.js +5 -2
- package/tests/unit/inspect.spec.ts +36 -0
- package/tests/unit/{spacer.spec.ts → spacer-description-joiner.spec.ts} +6 -6
- package/tests/unit/spec-description-tokenizer.spec.ts +100 -7
- package/tests/unit/spec-name-tokenizer.spec.ts +113 -1
- package/tests/unit/spec-parser.spec.ts +6 -9
- package/tests/unit/spec-tag-tokenizer.spec.ts +1 -1
- package/tests/unit/spec-type-tokenizer.spec.ts +121 -1
- package/tests/unit/stringifier.spec.ts +0 -1
- package/tests/unit/transforms-align.spec.ts +80 -16
- package/tests/unit/util-rewire.spec.ts +107 -0
- package/tests/unit/util.spec.ts +0 -48
- package/tsconfig.es6.json +1 -1
- package/tsconfig.node.json +1 -1
- package/es6/parser/spacer.d.ts +0 -3
- package/es6/parser/spacer.js +0 -37
- package/lib/parser/spacer.d.ts +0 -3
- package/lib/parser/spacer.js +0 -40
- package/src/parser/spacer.ts +0 -45
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { splitSpace, isSpace, seedSpec } from '../util';
|
|
2
1
|
import { Line, Spec } from '../primitives';
|
|
3
|
-
import {
|
|
2
|
+
import { seedSpec } from '../util';
|
|
3
|
+
import { Tokenizer } from './tokenizers/index';
|
|
4
4
|
|
|
5
5
|
export type Parser = (source: Line[]) => Spec;
|
|
6
6
|
|
|
7
|
-
export type Tokenizer = (spec: Spec) => Spec;
|
|
8
|
-
|
|
9
7
|
export interface Options {
|
|
10
8
|
tokenizers: Tokenizer[];
|
|
11
9
|
}
|
|
@@ -20,169 +18,3 @@ export default function getParser({ tokenizers }: Options): Parser {
|
|
|
20
18
|
return spec;
|
|
21
19
|
};
|
|
22
20
|
}
|
|
23
|
-
|
|
24
|
-
export function tagTokenizer(): Tokenizer {
|
|
25
|
-
return (spec: Spec): Spec => {
|
|
26
|
-
const { tokens } = spec.source[0];
|
|
27
|
-
const match = tokens.description.match(/\s*(@(\S+))(\s*)/);
|
|
28
|
-
|
|
29
|
-
if (match === null) {
|
|
30
|
-
spec.problems.push({
|
|
31
|
-
code: 'spec:tag:prefix',
|
|
32
|
-
message: 'tag should start with "@" symbol',
|
|
33
|
-
line: spec.source[0].number,
|
|
34
|
-
critical: true,
|
|
35
|
-
});
|
|
36
|
-
return spec;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
tokens.tag = match[1];
|
|
40
|
-
tokens.postTag = match[3];
|
|
41
|
-
tokens.description = tokens.description.slice(match[0].length);
|
|
42
|
-
|
|
43
|
-
spec.tag = match[2];
|
|
44
|
-
return spec;
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function typeTokenizer(): Tokenizer {
|
|
49
|
-
return (spec: Spec): Spec => {
|
|
50
|
-
let res = '';
|
|
51
|
-
let curlies = 0;
|
|
52
|
-
const { tokens } = spec.source[0];
|
|
53
|
-
const source = tokens.description.trimLeft();
|
|
54
|
-
|
|
55
|
-
if (source[0] !== '{') return spec;
|
|
56
|
-
|
|
57
|
-
for (const ch of source) {
|
|
58
|
-
if (ch === '{') curlies++;
|
|
59
|
-
if (ch === '}') curlies--;
|
|
60
|
-
res += ch;
|
|
61
|
-
if (curlies === 0) {
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (curlies !== 0) {
|
|
67
|
-
spec.problems.push({
|
|
68
|
-
code: 'spec:type:unpaired-curlies',
|
|
69
|
-
message: 'unpaired curlies',
|
|
70
|
-
line: spec.source[0].number,
|
|
71
|
-
critical: true,
|
|
72
|
-
});
|
|
73
|
-
return spec;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
spec.type = res.slice(1, -1);
|
|
77
|
-
tokens.type = res;
|
|
78
|
-
[tokens.postType, tokens.description] = splitSpace(
|
|
79
|
-
source.slice(tokens.type.length)
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
return spec;
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function nameTokenizer(): Tokenizer {
|
|
87
|
-
return (spec: Spec): Spec => {
|
|
88
|
-
const { tokens } = spec.source[0];
|
|
89
|
-
const source = tokens.description.trimLeft();
|
|
90
|
-
|
|
91
|
-
const quotedGroups = source.split('"');
|
|
92
|
-
|
|
93
|
-
// if it starts with quoted group, assume it is a literal
|
|
94
|
-
if (
|
|
95
|
-
quotedGroups.length > 1 &&
|
|
96
|
-
quotedGroups[0] === '' &&
|
|
97
|
-
quotedGroups.length % 2 === 1
|
|
98
|
-
) {
|
|
99
|
-
spec.name = quotedGroups[1];
|
|
100
|
-
tokens.name = `"${quotedGroups[1]}"`;
|
|
101
|
-
[tokens.postName, tokens.description] = splitSpace(
|
|
102
|
-
source.slice(tokens.name.length)
|
|
103
|
-
);
|
|
104
|
-
return spec;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
let brackets = 0;
|
|
108
|
-
let name = '';
|
|
109
|
-
let optional = false;
|
|
110
|
-
let defaultValue;
|
|
111
|
-
|
|
112
|
-
// assume name is non-space string or anything wrapped into brackets
|
|
113
|
-
for (const ch of source) {
|
|
114
|
-
if (brackets === 0 && isSpace(ch)) break;
|
|
115
|
-
if (ch === '[') brackets++;
|
|
116
|
-
if (ch === ']') brackets--;
|
|
117
|
-
name += ch;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (brackets !== 0) {
|
|
121
|
-
spec.problems.push({
|
|
122
|
-
code: 'spec:name:unpaired-brackets',
|
|
123
|
-
message: 'unpaired brackets',
|
|
124
|
-
line: spec.source[0].number,
|
|
125
|
-
critical: true,
|
|
126
|
-
});
|
|
127
|
-
return spec;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const nameToken = name;
|
|
131
|
-
|
|
132
|
-
if (name[0] === '[' && name[name.length - 1] === ']') {
|
|
133
|
-
optional = true;
|
|
134
|
-
name = name.slice(1, -1);
|
|
135
|
-
|
|
136
|
-
const parts = name.split('=');
|
|
137
|
-
name = parts[0].trim();
|
|
138
|
-
defaultValue = parts[1]?.trim();
|
|
139
|
-
|
|
140
|
-
if (name === '') {
|
|
141
|
-
spec.problems.push({
|
|
142
|
-
code: 'spec:name:empty-name',
|
|
143
|
-
message: 'empty name',
|
|
144
|
-
line: spec.source[0].number,
|
|
145
|
-
critical: true,
|
|
146
|
-
});
|
|
147
|
-
return spec;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (parts.length > 2) {
|
|
151
|
-
spec.problems.push({
|
|
152
|
-
code: 'spec:name:invalid-default',
|
|
153
|
-
message: 'invalid default value syntax',
|
|
154
|
-
line: spec.source[0].number,
|
|
155
|
-
critical: true,
|
|
156
|
-
});
|
|
157
|
-
return spec;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (defaultValue === '') {
|
|
161
|
-
spec.problems.push({
|
|
162
|
-
code: 'spec:name:empty-default',
|
|
163
|
-
message: 'empty default value',
|
|
164
|
-
line: spec.source[0].number,
|
|
165
|
-
critical: true,
|
|
166
|
-
});
|
|
167
|
-
return spec;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
spec.optional = optional;
|
|
172
|
-
spec.name = name;
|
|
173
|
-
tokens.name = nameToken;
|
|
174
|
-
|
|
175
|
-
if (defaultValue !== undefined) spec.default = defaultValue;
|
|
176
|
-
[tokens.postName, tokens.description] = splitSpace(
|
|
177
|
-
source.slice(tokens.name.length)
|
|
178
|
-
);
|
|
179
|
-
return spec;
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function descriptionTokenizer(join: Spacer): Tokenizer {
|
|
184
|
-
return (spec: Spec): Spec => {
|
|
185
|
-
spec.description = join(spec.source);
|
|
186
|
-
return spec;
|
|
187
|
-
};
|
|
188
|
-
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Spec, Line, Markers } from '../../primitives';
|
|
2
|
+
import { Tokenizer } from './index';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walks over provided lines joining description token into a single string.
|
|
6
|
+
* */
|
|
7
|
+
export type Joiner = (lines: Line[]) => string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shortcut for standard Joiners
|
|
11
|
+
* compact - strip surrounding whitespace and concat lines using a single string
|
|
12
|
+
* preserve - preserves original whitespace and line breaks as is
|
|
13
|
+
*/
|
|
14
|
+
export type Spacing = 'compact' | 'preserve' | Joiner;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Makes no changes to `spec.lines[].tokens` but joins them into `spec.description`
|
|
18
|
+
* following given spacing srtategy
|
|
19
|
+
* @param {Spacing} spacing tells how to handle the whitespace
|
|
20
|
+
*/
|
|
21
|
+
export default function descriptionTokenizer(
|
|
22
|
+
spacing: Spacing = 'compact'
|
|
23
|
+
): Tokenizer {
|
|
24
|
+
const join = getJoiner(spacing);
|
|
25
|
+
return (spec: Spec): Spec => {
|
|
26
|
+
spec.description = join(spec.source);
|
|
27
|
+
return spec;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getJoiner(spacing: Spacing): Joiner {
|
|
32
|
+
if (spacing === 'compact') return compactJoiner;
|
|
33
|
+
if (spacing === 'preserve') return preserveJoiner;
|
|
34
|
+
return spacing;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function compactJoiner(lines: Line[]): string {
|
|
38
|
+
return lines
|
|
39
|
+
.map(({ tokens: { description } }: Line) => description.trim())
|
|
40
|
+
.filter((description) => description !== '')
|
|
41
|
+
.join(' ');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lineNo = (num: number, { tokens }: Line, i: number) =>
|
|
45
|
+
tokens.type === '' ? num : i;
|
|
46
|
+
|
|
47
|
+
const getDescription = ({ tokens }: Line) =>
|
|
48
|
+
(tokens.delimiter === '' ? tokens.start : tokens.postDelimiter.slice(1)) +
|
|
49
|
+
tokens.description;
|
|
50
|
+
|
|
51
|
+
function preserveJoiner(lines: Line[]): string {
|
|
52
|
+
if (lines.length === 0) return '';
|
|
53
|
+
|
|
54
|
+
// skip the opening line with no description
|
|
55
|
+
if (
|
|
56
|
+
lines[0].tokens.description === '' &&
|
|
57
|
+
lines[0].tokens.delimiter === Markers.start
|
|
58
|
+
)
|
|
59
|
+
lines = lines.slice(1);
|
|
60
|
+
|
|
61
|
+
// skip the closing line with no description
|
|
62
|
+
const lastLine = lines[lines.length - 1];
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
lastLine !== undefined &&
|
|
66
|
+
lastLine.tokens.description === '' &&
|
|
67
|
+
lastLine.tokens.end.endsWith(Markers.end)
|
|
68
|
+
)
|
|
69
|
+
lines = lines.slice(0, -1);
|
|
70
|
+
|
|
71
|
+
// description starts at the last line of type definition
|
|
72
|
+
lines = lines.slice(lines.reduce(lineNo, 0));
|
|
73
|
+
|
|
74
|
+
return lines.map(getDescription).join('\n');
|
|
75
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Spec } from '../../primitives';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Splits `spect.lines[].token.description` into other tokens,
|
|
5
|
+
* and populates the spec.{tag, name, type, description}. Invoked in a chaing
|
|
6
|
+
* with other tokens, operations listed above can be moved to separate tokenizers
|
|
7
|
+
*/
|
|
8
|
+
export type Tokenizer = (spec: Spec) => Spec;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Spec, Line } from '../../primitives';
|
|
2
|
+
import { splitSpace, isSpace, seedBlock } from '../../util';
|
|
3
|
+
import { Tokenizer } from './index';
|
|
4
|
+
|
|
5
|
+
const isQuoted = (s: string) => s && s.startsWith('"') && s.endsWith('"');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Splits remaining `spec.lines[].tokens.description` into `name` and `descriptions` tokens,
|
|
9
|
+
* and populates the `spec.name`
|
|
10
|
+
*/
|
|
11
|
+
export default function nameTokenizer(): Tokenizer {
|
|
12
|
+
const typeEnd = (num: number, { tokens }: Line, i: number) =>
|
|
13
|
+
tokens.type === '' ? num : i;
|
|
14
|
+
|
|
15
|
+
return (spec: Spec): Spec => {
|
|
16
|
+
// look for the name in the line where {type} ends
|
|
17
|
+
const { tokens } = spec.source[spec.source.reduce(typeEnd, 0)];
|
|
18
|
+
const source = tokens.description.trimLeft();
|
|
19
|
+
|
|
20
|
+
const quotedGroups = source.split('"');
|
|
21
|
+
|
|
22
|
+
// if it starts with quoted group, assume it is a literal
|
|
23
|
+
if (
|
|
24
|
+
quotedGroups.length > 1 &&
|
|
25
|
+
quotedGroups[0] === '' &&
|
|
26
|
+
quotedGroups.length % 2 === 1
|
|
27
|
+
) {
|
|
28
|
+
spec.name = quotedGroups[1];
|
|
29
|
+
tokens.name = `"${quotedGroups[1]}"`;
|
|
30
|
+
[tokens.postName, tokens.description] = splitSpace(
|
|
31
|
+
source.slice(tokens.name.length)
|
|
32
|
+
);
|
|
33
|
+
return spec;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let brackets = 0;
|
|
37
|
+
let name = '';
|
|
38
|
+
let optional = false;
|
|
39
|
+
let defaultValue: string;
|
|
40
|
+
|
|
41
|
+
// assume name is non-space string or anything wrapped into brackets
|
|
42
|
+
for (const ch of source) {
|
|
43
|
+
if (brackets === 0 && isSpace(ch)) break;
|
|
44
|
+
if (ch === '[') brackets++;
|
|
45
|
+
if (ch === ']') brackets--;
|
|
46
|
+
name += ch;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (brackets !== 0) {
|
|
50
|
+
spec.problems.push({
|
|
51
|
+
code: 'spec:name:unpaired-brackets',
|
|
52
|
+
message: 'unpaired brackets',
|
|
53
|
+
line: spec.source[0].number,
|
|
54
|
+
critical: true,
|
|
55
|
+
});
|
|
56
|
+
return spec;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const nameToken = name;
|
|
60
|
+
|
|
61
|
+
if (name[0] === '[' && name[name.length - 1] === ']') {
|
|
62
|
+
optional = true;
|
|
63
|
+
name = name.slice(1, -1);
|
|
64
|
+
|
|
65
|
+
const parts = name.split('=');
|
|
66
|
+
name = parts[0].trim();
|
|
67
|
+
if (parts[1] !== undefined)
|
|
68
|
+
defaultValue = parts.slice(1).join('=').trim();
|
|
69
|
+
|
|
70
|
+
if (name === '') {
|
|
71
|
+
spec.problems.push({
|
|
72
|
+
code: 'spec:name:empty-name',
|
|
73
|
+
message: 'empty name',
|
|
74
|
+
line: spec.source[0].number,
|
|
75
|
+
critical: true,
|
|
76
|
+
});
|
|
77
|
+
return spec;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (defaultValue === '') {
|
|
81
|
+
spec.problems.push({
|
|
82
|
+
code: 'spec:name:empty-default',
|
|
83
|
+
message: 'empty default value',
|
|
84
|
+
line: spec.source[0].number,
|
|
85
|
+
critical: true,
|
|
86
|
+
});
|
|
87
|
+
return spec;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// has "=" and is not a string, except for "=>"
|
|
91
|
+
if (!isQuoted(defaultValue) && /=(?!>)/.test(defaultValue)) {
|
|
92
|
+
spec.problems.push({
|
|
93
|
+
code: 'spec:name:invalid-default',
|
|
94
|
+
message: 'invalid default value syntax',
|
|
95
|
+
line: spec.source[0].number,
|
|
96
|
+
critical: true,
|
|
97
|
+
});
|
|
98
|
+
return spec;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
spec.optional = optional;
|
|
103
|
+
spec.name = name;
|
|
104
|
+
tokens.name = nameToken;
|
|
105
|
+
|
|
106
|
+
if (defaultValue !== undefined) spec.default = defaultValue;
|
|
107
|
+
[tokens.postName, tokens.description] = splitSpace(
|
|
108
|
+
source.slice(tokens.name.length)
|
|
109
|
+
);
|
|
110
|
+
return spec;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Spec } from '../../primitives';
|
|
2
|
+
import { Tokenizer } from './index';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Splits the `@prefix` from remaining `Spec.lines[].token.descrioption` into the `tag` token,
|
|
6
|
+
* and populates `spec.tag`
|
|
7
|
+
*/
|
|
8
|
+
export default function tagTokenizer(): Tokenizer {
|
|
9
|
+
return (spec: Spec): Spec => {
|
|
10
|
+
const { tokens } = spec.source[0];
|
|
11
|
+
const match = tokens.description.match(/\s*(@(\S+))(\s*)/);
|
|
12
|
+
|
|
13
|
+
if (match === null) {
|
|
14
|
+
spec.problems.push({
|
|
15
|
+
code: 'spec:tag:prefix',
|
|
16
|
+
message: 'tag should start with "@" symbol',
|
|
17
|
+
line: spec.source[0].number,
|
|
18
|
+
critical: true,
|
|
19
|
+
});
|
|
20
|
+
return spec;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
tokens.tag = match[1];
|
|
24
|
+
tokens.postTag = match[3];
|
|
25
|
+
tokens.description = tokens.description.slice(match[0].length);
|
|
26
|
+
|
|
27
|
+
spec.tag = match[2];
|
|
28
|
+
return spec;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Spec, Line, Tokens } from '../../primitives';
|
|
2
|
+
import { splitSpace } from '../../util';
|
|
3
|
+
import { Tokenizer } from './index';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Joiner is a function taking collected type token string parts,
|
|
7
|
+
* and joining them together. In most of the cases this will be
|
|
8
|
+
* a single piece like {type-name}, but type may go over multipe line
|
|
9
|
+
* ```
|
|
10
|
+
* @tag {function(
|
|
11
|
+
* number,
|
|
12
|
+
* string
|
|
13
|
+
* )}
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export type Joiner = (parts: string[]) => string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Shortcut for standard Joiners
|
|
20
|
+
* compact - trim surrounding space, replace line breaks with a single space
|
|
21
|
+
* preserve - concat as is
|
|
22
|
+
*/
|
|
23
|
+
export type Spacing = 'compact' | 'preserve' | Joiner;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sets splits remaining `Spec.lines[].tokes.description` into `type` and `description`
|
|
27
|
+
* tokens and populates Spec.type`
|
|
28
|
+
*
|
|
29
|
+
* @param {Spacing} spacing tells how to deal with a whitespace
|
|
30
|
+
* for type values going over multiple lines
|
|
31
|
+
*/
|
|
32
|
+
export default function typeTokenizer(spacing: Spacing = 'compact'): Tokenizer {
|
|
33
|
+
const join = getJoiner(spacing);
|
|
34
|
+
return (spec: Spec): Spec => {
|
|
35
|
+
let curlies = 0;
|
|
36
|
+
let lines: [Tokens, string][] = [];
|
|
37
|
+
|
|
38
|
+
for (const [i, { tokens }] of spec.source.entries()) {
|
|
39
|
+
let type = '';
|
|
40
|
+
if (i === 0 && tokens.description[0] !== '{') return spec;
|
|
41
|
+
|
|
42
|
+
for (const ch of tokens.description) {
|
|
43
|
+
if (ch === '{') curlies++;
|
|
44
|
+
if (ch === '}') curlies--;
|
|
45
|
+
type += ch;
|
|
46
|
+
if (curlies === 0) break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lines.push([tokens, type]);
|
|
50
|
+
if (curlies === 0) break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (curlies !== 0) {
|
|
54
|
+
spec.problems.push({
|
|
55
|
+
code: 'spec:type:unpaired-curlies',
|
|
56
|
+
message: 'unpaired curlies',
|
|
57
|
+
line: spec.source[0].number,
|
|
58
|
+
critical: true,
|
|
59
|
+
});
|
|
60
|
+
return spec;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const parts: string[] = [];
|
|
64
|
+
const offset = lines[0][0].postDelimiter.length;
|
|
65
|
+
|
|
66
|
+
for (const [i, [tokens, type]] of lines.entries()) {
|
|
67
|
+
if (type === '') continue;
|
|
68
|
+
tokens.type = type;
|
|
69
|
+
if (i > 0) {
|
|
70
|
+
tokens.type = tokens.postDelimiter.slice(offset) + type;
|
|
71
|
+
tokens.postDelimiter = tokens.postDelimiter.slice(0, offset);
|
|
72
|
+
}
|
|
73
|
+
[tokens.postType, tokens.description] = splitSpace(
|
|
74
|
+
tokens.description.slice(type.length)
|
|
75
|
+
);
|
|
76
|
+
parts.push(tokens.type);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
parts[0] = parts[0].slice(1);
|
|
80
|
+
parts[parts.length - 1] = parts[parts.length - 1].slice(0, -1);
|
|
81
|
+
spec.type = join(parts);
|
|
82
|
+
return spec;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const trim = (x: string) => x.trim();
|
|
87
|
+
|
|
88
|
+
function getJoiner(spacing: Spacing): Joiner {
|
|
89
|
+
if (spacing === 'compact') return (t: string[]) => t.map(trim).join('');
|
|
90
|
+
else if (spacing === 'preserve') return (t: string[]) => t.join('\n');
|
|
91
|
+
else return spacing;
|
|
92
|
+
}
|
package/src/stringifier/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Block, Tokens } from '../primitives';
|
|
2
2
|
|
|
3
|
+
export type Stringifier = (block: Block) => string;
|
|
4
|
+
|
|
3
5
|
function join(tokens: Tokens): string {
|
|
4
6
|
return (
|
|
5
7
|
tokens.start +
|
|
@@ -16,7 +18,7 @@ function join(tokens: Tokens): string {
|
|
|
16
18
|
);
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export default function getStringifier() {
|
|
21
|
+
export default function getStringifier(): Stringifier {
|
|
20
22
|
return (block: Block): string =>
|
|
21
23
|
block.source.map(({ tokens }) => join(tokens)).join('\n');
|
|
22
24
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Block, Line, Tokens } from '../primitives';
|
|
2
|
+
import { seedTokens, isSpace } from '../util';
|
|
3
|
+
|
|
4
|
+
interface Width {
|
|
5
|
+
line: number;
|
|
6
|
+
start: number;
|
|
7
|
+
delimiter: number;
|
|
8
|
+
postDelimiter: number;
|
|
9
|
+
tag: number;
|
|
10
|
+
postTag: number;
|
|
11
|
+
name: number;
|
|
12
|
+
postName: number;
|
|
13
|
+
type: number;
|
|
14
|
+
postType: number;
|
|
15
|
+
description: number;
|
|
16
|
+
end: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const zeroWidth = {
|
|
20
|
+
line: 0,
|
|
21
|
+
start: 0,
|
|
22
|
+
delimiter: 0,
|
|
23
|
+
postDelimiter: 0,
|
|
24
|
+
tag: 0,
|
|
25
|
+
postTag: 0,
|
|
26
|
+
name: 0,
|
|
27
|
+
postName: 0,
|
|
28
|
+
type: 0,
|
|
29
|
+
postType: 0,
|
|
30
|
+
description: 0,
|
|
31
|
+
end: 0,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const fields = Object.keys(zeroWidth);
|
|
35
|
+
|
|
36
|
+
const repr = (x: string) => (isSpace(x) ? `{${x.length}}` : x);
|
|
37
|
+
|
|
38
|
+
const frame = (line: string[]) => '|' + line.join('|') + '|';
|
|
39
|
+
|
|
40
|
+
const align = (width: Width, tokens: Tokens): string[] =>
|
|
41
|
+
Object.keys(tokens).map((k) => repr(tokens[k]).padEnd(width[k]));
|
|
42
|
+
|
|
43
|
+
export default function inspect({ source }: Block): string {
|
|
44
|
+
if (source.length === 0) return '';
|
|
45
|
+
|
|
46
|
+
const width: Width = { ...zeroWidth };
|
|
47
|
+
|
|
48
|
+
for (const f of fields) width[f] = f.length;
|
|
49
|
+
for (const { number, tokens } of source) {
|
|
50
|
+
width.line = Math.max(width.line, number.toString().length);
|
|
51
|
+
for (const k in tokens)
|
|
52
|
+
width[k] = Math.max(width[k], repr(tokens[k]).length);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const lines: string[][] = [[], []];
|
|
56
|
+
for (const f of fields) lines[0].push(f.padEnd(width[f]));
|
|
57
|
+
for (const f of fields) lines[1].push('-'.padEnd(width[f], '-'));
|
|
58
|
+
for (const { number, tokens } of source) {
|
|
59
|
+
const line = number.toString().padStart(width.line);
|
|
60
|
+
lines.push([line, ...align(width, tokens)]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return lines.map(frame).join('\n');
|
|
64
|
+
}
|
package/src/transforms/align.ts
CHANGED
|
@@ -23,15 +23,6 @@ const getWidth = (w: Width, { tokens: t }: Line) => ({
|
|
|
23
23
|
name: Math.max(w.name, t.name.length),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
// /**
|
|
27
|
-
// * Description may go
|
|
28
|
-
// * over multiple lines followed by @tags
|
|
29
|
-
// *
|
|
30
|
-
//* @my-tag {my.type} my-name description line 1
|
|
31
|
-
// description line 2
|
|
32
|
-
// * description line 3
|
|
33
|
-
// */
|
|
34
|
-
|
|
35
26
|
const space = (len: number) => ''.padStart(len, ' ');
|
|
36
27
|
|
|
37
28
|
export default function align(): Transform {
|
|
@@ -62,15 +53,49 @@ export default function align(): Transform {
|
|
|
62
53
|
tokens.start = space(w.start + 1);
|
|
63
54
|
break;
|
|
64
55
|
default:
|
|
65
|
-
tokens.start = space(w.start + 3);
|
|
66
56
|
tokens.delimiter = '';
|
|
57
|
+
tokens.start = space(w.start + 2); // compensate delimiter
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!intoTags) {
|
|
61
|
+
tokens.postDelimiter = tokens.description === '' ? '' : ' ';
|
|
62
|
+
return { ...line, tokens };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const nothingAfter = {
|
|
66
|
+
delim: false,
|
|
67
|
+
tag: false,
|
|
68
|
+
type: false,
|
|
69
|
+
name: false,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (tokens.description === '') {
|
|
73
|
+
nothingAfter.name = true;
|
|
74
|
+
tokens.postName = '';
|
|
75
|
+
|
|
76
|
+
if (tokens.name === '') {
|
|
77
|
+
nothingAfter.type = true;
|
|
78
|
+
tokens.postType = '';
|
|
79
|
+
|
|
80
|
+
if (tokens.type === '') {
|
|
81
|
+
nothingAfter.tag = true;
|
|
82
|
+
tokens.postTag = '';
|
|
83
|
+
|
|
84
|
+
if (tokens.tag === '') {
|
|
85
|
+
nothingAfter.delim = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
67
89
|
}
|
|
68
90
|
|
|
69
|
-
|
|
91
|
+
tokens.postDelimiter = nothingAfter.delim ? '' : ' ';
|
|
92
|
+
|
|
93
|
+
if (!nothingAfter.tag)
|
|
70
94
|
tokens.postTag = space(w.tag - tokens.tag.length + 1);
|
|
95
|
+
if (!nothingAfter.type)
|
|
71
96
|
tokens.postType = space(w.type - tokens.type.length + 1);
|
|
97
|
+
if (!nothingAfter.name)
|
|
72
98
|
tokens.postName = space(w.name - tokens.name.length + 1);
|
|
73
|
-
}
|
|
74
99
|
|
|
75
100
|
return { ...line, tokens };
|
|
76
101
|
}
|
package/src/transforms/index.ts
CHANGED
|
@@ -2,9 +2,6 @@ import { Block } from '../primitives';
|
|
|
2
2
|
|
|
3
3
|
export type Transform = (Block) => Block;
|
|
4
4
|
|
|
5
|
-
export { default as indent } from './indent';
|
|
6
|
-
export { default as align } from './align';
|
|
7
|
-
|
|
8
5
|
export function flow(...transforms: Transform[]): Transform {
|
|
9
6
|
return (block: Block): Block =>
|
|
10
7
|
transforms.reduce((block, t) => t(block), block);
|
package/src/util.ts
CHANGED
|
@@ -55,6 +55,11 @@ export function seedTokens(tokens: Partial<Tokens> = {}): Tokens {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Assures Block.tags[].source contains references to the Block.source items,
|
|
60
|
+
* using Block.source as a source of truth. This is a counterpart of rewireSpecs
|
|
61
|
+
* @param block parsed coments block
|
|
62
|
+
*/
|
|
58
63
|
export function rewireSource(block: Block): Block {
|
|
59
64
|
const source = block.source.reduce(
|
|
60
65
|
(acc, line) => acc.set(line.number, line),
|
|
@@ -65,3 +70,18 @@ export function rewireSource(block: Block): Block {
|
|
|
65
70
|
}
|
|
66
71
|
return block;
|
|
67
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Assures Block.source contains references to the Block.tags[].source items,
|
|
76
|
+
* using Block.tags[].source as a source of truth. This is a counterpart of rewireSource
|
|
77
|
+
* @param block parsed coments block
|
|
78
|
+
*/
|
|
79
|
+
export function rewireSpecs(block: Block): Block {
|
|
80
|
+
const source = block.tags.reduce(
|
|
81
|
+
(acc, spec) =>
|
|
82
|
+
spec.source.reduce((acc, line) => acc.set(line.number, line), acc),
|
|
83
|
+
new Map<number, Line>()
|
|
84
|
+
);
|
|
85
|
+
block.source = block.source.map((line) => source.get(line.number) || line);
|
|
86
|
+
return block;
|
|
87
|
+
}
|