Skip to content

Commit ccab54a

Browse files
authored
feat(#250): introduce caseFreeTags option (#251)
* chore: initial tests * feat: parser test * feat: add case free tags support * fix: coverage upload * fix: --disable=gcov * fix: npm publish sha commit * fix: change codecov to coveralls * fix: change workflow pr build and publish * chore: change coverage badge [skip ci]
1 parent 99c629e commit ccab54a

File tree

12 files changed

+293
-160
lines changed

12 files changed

+293
-160
lines changed

.changeset/poor-pears-marry.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
"@bbob/parser": minor
3+
"@bbob/types": minor
4+
"@bbob/cli": minor
5+
"@bbob/core": minor
6+
"@bbob/html": minor
7+
"@bbob/plugin-helper": minor
8+
"@bbob/preset": minor
9+
"@bbob/preset-html5": minor
10+
"@bbob/preset-react": minor
11+
"@bbob/preset-vue": minor
12+
"@bbob/react": minor
13+
"@bbob/vue2": minor
14+
"@bbob/vue3": minor
15+
---
16+
17+
New option flag `caseFreeTags` has been added
18+
19+
This flag allows to parse case insensitive tags like `[h1]some[/H1]` -> `<h1>some</h1>`
20+
21+
```js
22+
import html from '@bbob/html'
23+
import presetHTML5 from '@bbob/preset-html5'
24+
25+
const processed = html(`[h1]some[/H1]`, presetHTML5(), { caseFreeTags: true })
26+
27+
console.log(processed); // <h1>some</h1>
28+
```
29+
30+
Also now you can pass `caseFreeTags` to `parse` function
31+
32+
```js
33+
import { parse } from '@bbob/parser'
34+
35+
const ast = parse('[h1]some[/H1]', {
36+
caseFreeTags: true
37+
});
38+
```
39+
40+
BREAKING CHANGE: `isTokenNested` function now accepts string `tokenValue` instead of `token`
41+
42+
Changed codecov.io to coveralls.io for test coverage

.github/workflows/pr.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
name: Pull Request
22
on:
3+
# workflow_run:
4+
# workflows:
5+
# - Tests
6+
# - Benchmark
7+
# types:
8+
# - completed
39
pull_request:
410
paths-ignore:
511
- '.changeset/**'
612
- '.husky/**'
13+
workflow_dispatch:
714

815
concurrency:
916
group: ci-pull-request=${{github.ref}}-1
@@ -30,7 +37,7 @@ jobs:
3037
- name: Set SHA
3138
id: sha
3239
run: |
33-
SHORT_SHA=$(git rev-parse --short "$GITHUB_SHA")
40+
SHORT_SHA=$(git rev-parse --short "${{ github.event.pull_request.head.sha }}")
3441
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
3542
3643
- name: Install pnpm

.github/workflows/test.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,8 @@ jobs:
3737
- name: Run the lint
3838
run: pnpm run lint
3939

40-
- name: Install coverage
41-
run: pnpm install --global codecov
42-
4340
- name: Run the coverage
4441
run: pnpm run cover
4542

46-
- name: Run the coverage
47-
run: codecov
43+
- name: Coveralls
44+
uses: coverallsapp/github-action@v2

README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ written in pure javascript, no dependencies
88

99
[![Tests](https://github.com/JiLiZART/BBob/actions/workflows/test.yml/badge.svg)](https://github.com/JiLiZART/BBob/actions/workflows/test.yml)
1010
[![Benchmark](https://github.com/JiLiZART/BBob/actions/workflows/benchmark.yml/badge.svg)](https://github.com/JiLiZART/BBob/actions/workflows/benchmark.yml)
11-
<a href="https://codecov.io/gh/JiLiZART/bbob">
12-
<img src="https://codecov.io/gh/JiLiZART/bbob/branch/master/graph/badge.svg" alt="codecov">
13-
</a>
11+
<a href='https://coveralls.io/github/JiLiZART/BBob?branch=master'>
12+
<img src='https://coveralls.io/repos/github/JiLiZART/BBob/badge.svg?branch=master' alt='Coverage Status' />
13+
</a>
1414
<a href="https://www.codefactor.io/repository/github/jilizart/bbob">
1515
<img src="https://www.codefactor.io/repository/github/jilizart/bbob/badge" alt="CodeFactor">
1616
</a>
@@ -230,6 +230,28 @@ const processed = bbobHTML(`[b]Text[/b]'\\[b\\]Text\\[/b\\]'`, presetHTML5(), {
230230
console.log(processed); // <span style="font-weight: bold;">Text</span>[b]Text[/b]
231231
```
232232

233+
#### caseFreeTags
234+
235+
Allows to parse case insensitive tags like `[h1]some[/H1]` -> `<h1>some</h1>`
236+
237+
```js
238+
import bbobHTML from '@bbob/html'
239+
import presetHTML5 from '@bbob/preset-html5'
240+
241+
const processed = bbobHTML(`[h1]some[/H1]`, presetHTML5(), { caseFreeTags: true })
242+
243+
console.log(processed); // <h1>some</h1>
244+
```
245+
246+
```js
247+
import bbobHTML from '@bbob/html'
248+
import presetHTML5 from '@bbob/preset-html5'
249+
250+
const processed = bbobHTML(`[b]Text[/b]'\\[b\\]Text\\[/b\\]'`, presetHTML5(), { enableEscapeTags: true })
251+
252+
console.log(processed); // <span style="font-weight: bold;">Text</span>[b]Text[/b]
253+
```
254+
233255

234256
### Presets <a name="basic"></a>
235257

benchmark/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
"cpupro": "node --require cpupro benchmark.js"
1111
},
1212
"author": {
13-
"name": "Nikolay Kostyurin <jilizart@gmail.com>",
14-
"url": "https://artkost.ru/"
13+
"name": "Nikolay Kostyurin <jilizart@gmail.com>"
1514
},
1615
"dependencies": {
1716
"@bbob/parser": "*",

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
"cleanup": "node scripts/cleanup"
2121
},
2222
"author": {
23-
"name": "Nikolay Kostyurin <jilizart@gmail.com>",
24-
"url": "https://artkost.ru/"
23+
"name": "Nikolay Kostyurin <jilizart@gmail.com>"
2524
},
2625
"license": "MIT",
2726
"devDependencies": {

packages/bbob-parser/src/Token.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ const getTagName = (token: Token) => {
8181
return isTagEnd(token) ? value.slice(1) : value;
8282
};
8383

84-
const tokenToText = (token: Token) => {
85-
let text = OPEN_BRAKET;
84+
const tokenToText = (token: Token, openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET) => {
85+
let text = openTag;
8686

8787
text += getTokenValue(token);
88-
text += CLOSE_BRAKET;
88+
text += closeTag;
8989

9090
return text;
9191
};
@@ -167,8 +167,8 @@ class Token<TokenValue = string> implements TokenInterface {
167167
return getEndPosition(this);
168168
}
169169

170-
toString() {
171-
return tokenToText(this);
170+
toString({ openTag = OPEN_BRAKET, closeTag = CLOSE_BRAKET } = {}) {
171+
return tokenToText(this, openTag, closeTag);
172172
}
173173
}
174174

packages/bbob-parser/src/lexer.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,14 @@ export function createLexer(buffer: string, options: LexerOptions = {}): LexerTo
5151
let stateMode = STATE_WORD;
5252
let tagMode = TAG_STATE_NAME;
5353
let contextFreeTag = '';
54-
const tokens = new Array<Token<string>>(Math.floor(buffer.length));
54+
const tokens = new Array<Token>(Math.floor(buffer.length));
5555
const openTag = options.openTag || OPEN_BRAKET;
5656
const closeTag = options.closeTag || CLOSE_BRAKET;
5757
const escapeTags = !!options.enableEscapeTags;
5858
const contextFreeTags = (options.contextFreeTags || [])
5959
.filter(Boolean)
6060
.map((tag) => tag.toLowerCase());
61+
const caseFreeTags = options.caseFreeTags || false;
6162
const nestedMap = new Map<string, boolean>();
6263
const onToken = options.onToken || (() => {
6364
});
@@ -88,8 +89,6 @@ export function createLexer(buffer: string, options: LexerOptions = {}): LexerTo
8889

8990
/**
9091
* Emits newly created token to subscriber
91-
* @param {Number} type
92-
* @param {String} value
9392
*/
9493
function emitToken(type: number, value: string, startPos?: number, endPos?: number) {
9594
const token = createTokenOfType(type, value, row, prevCol, startPos, endPos);
@@ -352,13 +351,13 @@ export function createLexer(buffer: string, options: LexerOptions = {}): LexerTo
352351
return tokens;
353352
}
354353

355-
function isTokenNested(token: Token) {
356-
const value = openTag + SLASH + token.getValue();
354+
function isTokenNested(tokenValue: string) {
355+
const value = openTag + SLASH + tokenValue;
357356

358357
if (nestedMap.has(value)) {
359358
return !!nestedMap.get(value);
360359
} else {
361-
const status = (buffer.indexOf(value) > -1);
360+
const status = caseFreeTags ? (buffer.toLowerCase().indexOf(value.toLowerCase()) > -1) : (buffer.indexOf(value) > -1);
362361

363362
nestedMap.set(value, status);
364363

packages/bbob-parser/src/parse.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ function parse(input: string, opts: ParseOptions = {}) {
5252
const onlyAllowTags = (options.onlyAllowTags || [])
5353
.filter(Boolean)
5454
.map((tag) => tag.toLowerCase());
55+
const caseFreeTags = options.caseFreeTags || false;
5556

56-
let tokenizer: LexerTokenizer | null = null;
57+
let tokenizer: ReturnType<typeof createLexer> | null = null;
5758

5859
/**
5960
* Result AST of nodes
@@ -85,10 +86,11 @@ function parse(input: string, opts: ParseOptions = {}) {
8586
const nestedTagsMap = new Set<string>();
8687

8788
function isTokenNested(token: Token) {
88-
const value = token.getValue();
89+
const tokenValue = token.getValue();
90+
const value = caseFreeTags ? tokenValue.toLowerCase() : tokenValue;
8991
const { isTokenNested } = tokenizer || {};
9092

91-
if (!nestedTagsMap.has(value) && isTokenNested && isTokenNested(token)) {
93+
if (!nestedTagsMap.has(value) && isTokenNested && isTokenNested(value)) {
9294
nestedTagsMap.add(value);
9395

9496
return true;
@@ -101,7 +103,7 @@ function parse(input: string, opts: ParseOptions = {}) {
101103
* @private
102104
*/
103105
function isTagNested(tagName: string) {
104-
return Boolean(nestedTagsMap.has(tagName));
106+
return Boolean(nestedTagsMap.has(caseFreeTags ? tagName.toLowerCase() : tagName));
105107
}
106108

107109
/**
@@ -203,17 +205,23 @@ function parse(input: string, opts: ParseOptions = {}) {
203205
* @param {Token} token
204206
*/
205207
function handleTagEnd(token: Token) {
206-
const lastTagNode = nestedNodes.last();
207-
if (isTagNode(lastTagNode)) {
208-
lastTagNode.setEnd({ from: token.getStart(), to: token.getEnd() });
209-
}
210-
flushTagNodes();
211-
208+
const tagName = token.getValue().slice(1);
212209
const lastNestedNode = nestedNodes.flush();
213210

211+
flushTagNodes();
212+
214213
if (lastNestedNode) {
215214
const nodes = getNodes();
215+
216+
if (isTagNode(lastNestedNode)) {
217+
lastNestedNode.setEnd({ from: token.getStart(), to: token.getEnd() });
218+
}
219+
216220
appendNodes(nodes, lastNestedNode);
221+
} else if (!isTagNested(tagName)) { // when we have only close tag [/some] without any open tag
222+
const nodes = getNodes();
223+
224+
appendNodes(nodes, token.toString({ openTag, closeTag }));
217225
} else if (typeof options.onError === "function") {
218226
const tag = token.getValue();
219227
const line = token.getLine();
@@ -281,13 +289,13 @@ function parse(input: string, opts: ParseOptions = {}) {
281289
}
282290
} else if (token.isTag()) {
283291
// if tag is not allowed, just pass it as is
284-
appendNodes(nodes, token.toString());
292+
appendNodes(nodes, token.toString({ openTag, closeTag }));
285293
}
286294
} else if (token.isText()) {
287295
appendNodes(nodes, tokenValue);
288296
} else if (token.isTag()) {
289297
// if tag is not allowed, just pass it as is
290-
appendNodes(nodes, token.toString());
298+
appendNodes(nodes, token.toString({ openTag, closeTag }));
291299
}
292300
}
293301

@@ -311,6 +319,7 @@ function parse(input: string, opts: ParseOptions = {}) {
311319
closeTag,
312320
onlyAllowTags: options.onlyAllowTags,
313321
contextFreeTags: options.contextFreeTags,
322+
caseFreeTags: options.caseFreeTags,
314323
enableEscapeTags: options.enableEscapeTags,
315324
});
316325

0 commit comments

Comments
 (0)