@yozora/tokenizer-table 2.0.4 → 2.0.5
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/lib/cjs/{index.js → index.cjs} +6 -7
- package/lib/esm/{index.js → index.mjs} +5 -6
- package/lib/types/index.d.ts +4 -4
- package/package.json +18 -14
- package/src/index.ts +11 -0
- package/src/match.ts +416 -0
- package/src/parse.ts +53 -0
- package/src/tokenizer.ts +33 -0
- package/src/types.ts +42 -0
|
@@ -180,7 +180,7 @@ const match = function (api) {
|
|
|
180
180
|
for (let c = cells.length; c < columns.length; ++c) {
|
|
181
181
|
const cell = {
|
|
182
182
|
nodeType: ast.TableCellType,
|
|
183
|
-
position: { start:
|
|
183
|
+
position: { start: { ...endPoint }, end: { ...endPoint } },
|
|
184
184
|
lines: [],
|
|
185
185
|
};
|
|
186
186
|
cells.push(cell);
|
|
@@ -237,18 +237,17 @@ const uniqueName = '@yozora/tokenizer-table';
|
|
|
237
237
|
|
|
238
238
|
class TableTokenizer extends coreTokenizer.BaseBlockTokenizer {
|
|
239
239
|
constructor(props = {}) {
|
|
240
|
-
var _a, _b;
|
|
241
240
|
super({
|
|
242
|
-
name:
|
|
243
|
-
priority:
|
|
241
|
+
name: props.name ?? uniqueName,
|
|
242
|
+
priority: props.priority ?? coreTokenizer.TokenizerPriority.INTERRUPTABLE_BLOCK,
|
|
244
243
|
});
|
|
245
|
-
this.match = match;
|
|
246
|
-
this.parse = parse;
|
|
247
244
|
}
|
|
245
|
+
match = match;
|
|
246
|
+
parse = parse;
|
|
248
247
|
}
|
|
249
248
|
|
|
250
249
|
exports.TableTokenizer = TableTokenizer;
|
|
251
250
|
exports.TableTokenizerName = uniqueName;
|
|
252
|
-
exports
|
|
251
|
+
exports.default = TableTokenizer;
|
|
253
252
|
exports.tableMatch = match;
|
|
254
253
|
exports.tableParse = parse;
|
|
@@ -176,7 +176,7 @@ const match = function (api) {
|
|
|
176
176
|
for (let c = cells.length; c < columns.length; ++c) {
|
|
177
177
|
const cell = {
|
|
178
178
|
nodeType: TableCellType,
|
|
179
|
-
position: { start:
|
|
179
|
+
position: { start: { ...endPoint }, end: { ...endPoint } },
|
|
180
180
|
lines: [],
|
|
181
181
|
};
|
|
182
182
|
cells.push(cell);
|
|
@@ -233,14 +233,13 @@ const uniqueName = '@yozora/tokenizer-table';
|
|
|
233
233
|
|
|
234
234
|
class TableTokenizer extends BaseBlockTokenizer {
|
|
235
235
|
constructor(props = {}) {
|
|
236
|
-
var _a, _b;
|
|
237
236
|
super({
|
|
238
|
-
name:
|
|
239
|
-
priority:
|
|
237
|
+
name: props.name ?? uniqueName,
|
|
238
|
+
priority: props.priority ?? TokenizerPriority.INTERRUPTABLE_BLOCK,
|
|
240
239
|
});
|
|
241
|
-
this.match = match;
|
|
242
|
-
this.parse = parse;
|
|
243
240
|
}
|
|
241
|
+
match = match;
|
|
242
|
+
parse = parse;
|
|
244
243
|
}
|
|
245
244
|
|
|
246
245
|
export { TableTokenizer, uniqueName as TableTokenizerName, TableTokenizer as default, match as tableMatch, parse as tableParse };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IPartialYastBlockToken, ITokenizer, IBaseBlockTokenizerProps, IPhrasingContentLine, IMatchBlockHookCreator, IParseBlockHookCreator, BaseBlockTokenizer, IBlockTokenizer } from '@yozora/core-tokenizer';
|
|
2
2
|
import { TableType, TableColumn, TableRowType, TableCellType, Position, Table } from '@yozora/ast';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
type T = TableType;
|
|
5
|
+
type INode = Table;
|
|
6
6
|
declare const uniqueName = "@yozora/tokenizer-table";
|
|
7
7
|
interface IToken extends IPartialYastBlockToken<TableType> {
|
|
8
8
|
/**
|
|
@@ -14,8 +14,8 @@ interface IToken extends IPartialYastBlockToken<TableType> {
|
|
|
14
14
|
*/
|
|
15
15
|
rows: ITableRowToken[];
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
type IThis = ITokenizer;
|
|
18
|
+
type ITokenizerProps = Partial<IBaseBlockTokenizerProps>;
|
|
19
19
|
interface ITableRowToken extends IPartialYastBlockToken<TableRowType> {
|
|
20
20
|
cells: ITableCellToken[];
|
|
21
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yozora/tokenizer-table",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "guanghechen",
|
|
6
6
|
"url": "https://github.com/guanghechen/"
|
|
@@ -11,33 +11,37 @@
|
|
|
11
11
|
"directory": "tokenizers/table"
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/yozorajs/yozora/tree/release-2.x.x/tokenizers/table",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
"types": "./lib/types/index.d.ts",
|
|
17
|
+
"import": "./lib/esm/index.mjs",
|
|
18
|
+
"require": "./lib/cjs/index.cjs"
|
|
19
|
+
},
|
|
20
|
+
"source": "./src/index.ts",
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"main": "./lib/cjs/index.cjs",
|
|
23
|
+
"module": "./lib/esm/index.mjs",
|
|
18
24
|
"license": "MIT",
|
|
19
25
|
"engines": {
|
|
20
26
|
"node": ">= 16.0.0"
|
|
21
27
|
},
|
|
22
28
|
"files": [
|
|
23
29
|
"lib/",
|
|
24
|
-
"
|
|
25
|
-
"!lib/**/*.d.ts.map",
|
|
30
|
+
"src/",
|
|
26
31
|
"package.json",
|
|
27
32
|
"CHANGELOG.md",
|
|
28
33
|
"LICENSE",
|
|
29
34
|
"README.md"
|
|
30
35
|
],
|
|
31
36
|
"scripts": {
|
|
32
|
-
"build": "cross-env NODE_ENV=production rollup -c ../../rollup.config.
|
|
33
|
-
"prebuild": "rimraf lib/",
|
|
37
|
+
"build": "rimraf lib/ && cross-env NODE_ENV=production rollup -c ../../rollup.config.mjs",
|
|
34
38
|
"prepublishOnly": "cross-env ROLLUP_SHOULD_SOURCEMAP=false yarn build",
|
|
35
|
-
"test": "cross-env TS_NODE_FILES=true jest --config ../../jest.config.
|
|
39
|
+
"test": "cross-env TS_NODE_FILES=true NODE_OPTIONS=--experimental-vm-modules jest --config ../../jest.config.mjs --rootDir ."
|
|
36
40
|
},
|
|
37
41
|
"dependencies": {
|
|
38
|
-
"@yozora/ast": "^2.0.
|
|
39
|
-
"@yozora/character": "^2.0.
|
|
40
|
-
"@yozora/core-tokenizer": "^2.0.
|
|
42
|
+
"@yozora/ast": "^2.0.5",
|
|
43
|
+
"@yozora/character": "^2.0.5",
|
|
44
|
+
"@yozora/core-tokenizer": "^2.0.5"
|
|
41
45
|
},
|
|
42
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "7ba3bab49fe65cf2f57082c0503af73da9356cf0"
|
|
43
47
|
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { match as tableMatch } from './match'
|
|
2
|
+
export { parse as tableParse } from './parse'
|
|
3
|
+
export { TableTokenizer, TableTokenizer as default } from './tokenizer'
|
|
4
|
+
export { uniqueName as TableTokenizerName } from './types'
|
|
5
|
+
export type {
|
|
6
|
+
IThis as ITableHookContext,
|
|
7
|
+
ITableCellToken,
|
|
8
|
+
ITableRowToken,
|
|
9
|
+
IToken as ITableToken,
|
|
10
|
+
ITokenizerProps as ITableTokenizerProps,
|
|
11
|
+
} from './types'
|
package/src/match.ts
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import type { AlignType, Point, TableColumn } from '@yozora/ast'
|
|
2
|
+
import { TableCellType, TableRowType, TableType } from '@yozora/ast'
|
|
3
|
+
import { AsciiCodePoint, isWhitespaceCharacter } from '@yozora/character'
|
|
4
|
+
import type {
|
|
5
|
+
IMatchBlockHookCreator,
|
|
6
|
+
IPhrasingContentLine,
|
|
7
|
+
IResultOfEatAndInterruptPreviousSibling,
|
|
8
|
+
IResultOfEatLazyContinuationText,
|
|
9
|
+
IResultOfEatOpener,
|
|
10
|
+
IYastBlockToken,
|
|
11
|
+
} from '@yozora/core-tokenizer'
|
|
12
|
+
import { calcEndPoint, calcStartPoint } from '@yozora/core-tokenizer'
|
|
13
|
+
import type { ITableCellToken, ITableRowToken, IThis, IToken, T } from './types'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A table is an arrangement of data with rows and columns, consisting of
|
|
17
|
+
* a single header row, a delimiter row separating the header from the data,
|
|
18
|
+
* and zero or more data rows.
|
|
19
|
+
*
|
|
20
|
+
* Each row consists of cells containing arbitrary text, in which inlines
|
|
21
|
+
* are parsed, separated by pipes (|). A leading and trailing pipe is also
|
|
22
|
+
* recommended for clarity of reading, and if there’s otherwise parsing
|
|
23
|
+
* ambiguity. Spaces between pipes and cell content are trimmed. Block-level
|
|
24
|
+
* elements cannot be inserted in a table.
|
|
25
|
+
*
|
|
26
|
+
* @see https://github.github.com/gfm/#table
|
|
27
|
+
* @see https://github.com/syntax-tree/mdast#tablerow
|
|
28
|
+
* @see https://github.com/syntax-tree/mdast#tablecell
|
|
29
|
+
*/
|
|
30
|
+
export const match: IMatchBlockHookCreator<T, IToken, IThis> = function (api) {
|
|
31
|
+
return {
|
|
32
|
+
isContainingBlock: false,
|
|
33
|
+
eatOpener,
|
|
34
|
+
eatAndInterruptPreviousSibling,
|
|
35
|
+
eatLazyContinuationText,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function eatOpener(): IResultOfEatOpener<T, IToken> {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function eatAndInterruptPreviousSibling(
|
|
43
|
+
line: Readonly<IPhrasingContentLine>,
|
|
44
|
+
prevSiblingToken: Readonly<IYastBlockToken>,
|
|
45
|
+
): IResultOfEatAndInterruptPreviousSibling<T, IToken> {
|
|
46
|
+
/**
|
|
47
|
+
* Four spaces is too much
|
|
48
|
+
* @see https://github.github.com/gfm/#example-57
|
|
49
|
+
*/
|
|
50
|
+
if (line.countOfPrecedeSpaces >= 4) return null
|
|
51
|
+
|
|
52
|
+
const { nodePoints, endIndex, firstNonWhitespaceIndex } = line
|
|
53
|
+
if (firstNonWhitespaceIndex >= endIndex) return null
|
|
54
|
+
|
|
55
|
+
const columns: TableColumn[] = []
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* eat leading optional pipe
|
|
59
|
+
*/
|
|
60
|
+
let c = nodePoints[firstNonWhitespaceIndex].codePoint
|
|
61
|
+
let cIndex =
|
|
62
|
+
c === AsciiCodePoint.VERTICAL_SLASH ? firstNonWhitespaceIndex + 1 : firstNonWhitespaceIndex
|
|
63
|
+
for (; cIndex < endIndex; ) {
|
|
64
|
+
for (; cIndex < endIndex; ++cIndex) {
|
|
65
|
+
c = nodePoints[cIndex].codePoint
|
|
66
|
+
if (!isWhitespaceCharacter(c)) break
|
|
67
|
+
}
|
|
68
|
+
if (cIndex >= endIndex) break
|
|
69
|
+
|
|
70
|
+
// eat left optional colon
|
|
71
|
+
let leftColon = false
|
|
72
|
+
if (c === AsciiCodePoint.COLON) {
|
|
73
|
+
leftColon = true
|
|
74
|
+
cIndex += 1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let hyphenCount = 0
|
|
78
|
+
for (; cIndex < endIndex; ++cIndex) {
|
|
79
|
+
c = nodePoints[cIndex].codePoint
|
|
80
|
+
if (c !== AsciiCodePoint.MINUS_SIGN) break
|
|
81
|
+
hyphenCount += 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// hyphen must be exist
|
|
85
|
+
if (hyphenCount <= 0) return null
|
|
86
|
+
|
|
87
|
+
// eat right optional colon
|
|
88
|
+
let rightColon = false
|
|
89
|
+
if (cIndex < endIndex && c === AsciiCodePoint.COLON) {
|
|
90
|
+
rightColon = true
|
|
91
|
+
cIndex += 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// eating next pipe
|
|
95
|
+
for (; cIndex < endIndex; ++cIndex) {
|
|
96
|
+
c = nodePoints[cIndex].codePoint
|
|
97
|
+
if (isWhitespaceCharacter(c)) continue
|
|
98
|
+
if (c === AsciiCodePoint.VERTICAL_SLASH) {
|
|
99
|
+
cIndex += 1
|
|
100
|
+
break
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// There are other non-white space characters
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let align: AlignType = null
|
|
108
|
+
if (leftColon && rightColon) align = 'center'
|
|
109
|
+
else if (leftColon) align = 'left'
|
|
110
|
+
else if (rightColon) align = 'right'
|
|
111
|
+
const column: TableColumn = { align }
|
|
112
|
+
columns.push(column)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (columns.length <= 0) return null
|
|
116
|
+
|
|
117
|
+
const lines = api.extractPhrasingLines(prevSiblingToken)
|
|
118
|
+
if (lines == null || lines.length < 1) return null
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The header row must match the delimiter row in the number of cells.
|
|
122
|
+
* If not, a table will not be recognized
|
|
123
|
+
* @see https://github.github.com/gfm/#example-203
|
|
124
|
+
*/
|
|
125
|
+
let cellCount = 0,
|
|
126
|
+
hasNonWhitespaceBeforePipe = false
|
|
127
|
+
const previousLine = lines[lines.length - 1]
|
|
128
|
+
for (let pIndex = previousLine.startIndex; pIndex < previousLine.endIndex; ++pIndex) {
|
|
129
|
+
const p = nodePoints[pIndex]
|
|
130
|
+
if (isWhitespaceCharacter(p.codePoint)) continue
|
|
131
|
+
|
|
132
|
+
if (p.codePoint === AsciiCodePoint.VERTICAL_SLASH) {
|
|
133
|
+
if (hasNonWhitespaceBeforePipe || cellCount > 0) cellCount += 1
|
|
134
|
+
hasNonWhitespaceBeforePipe = false
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
hasNonWhitespaceBeforePipe = true
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Include a pipe in a cell’s content by escaping it,
|
|
142
|
+
* including inside other inline spans
|
|
143
|
+
*/
|
|
144
|
+
if (p.codePoint === AsciiCodePoint.BACKSLASH) pIndex += 1
|
|
145
|
+
}
|
|
146
|
+
if (hasNonWhitespaceBeforePipe && columns.length > 1) cellCount += 1
|
|
147
|
+
if (cellCount !== columns.length) return null
|
|
148
|
+
|
|
149
|
+
const row = calcTableRow(previousLine, columns)
|
|
150
|
+
const nextIndex = endIndex
|
|
151
|
+
const token: IToken = {
|
|
152
|
+
nodeType: TableType,
|
|
153
|
+
position: {
|
|
154
|
+
start: calcStartPoint(previousLine.nodePoints, previousLine.startIndex),
|
|
155
|
+
end: calcEndPoint(nodePoints, nextIndex - 1),
|
|
156
|
+
},
|
|
157
|
+
columns,
|
|
158
|
+
rows: [row],
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
token,
|
|
162
|
+
nextIndex,
|
|
163
|
+
remainingSibling: api.rollbackPhrasingLines(
|
|
164
|
+
lines.slice(0, lines.length - 1),
|
|
165
|
+
prevSiblingToken,
|
|
166
|
+
),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function eatLazyContinuationText(
|
|
171
|
+
line: Readonly<IPhrasingContentLine>,
|
|
172
|
+
token: IToken,
|
|
173
|
+
): IResultOfEatLazyContinuationText {
|
|
174
|
+
if (line.firstNonWhitespaceIndex >= line.endIndex) {
|
|
175
|
+
return { status: 'notMatched' }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const row = calcTableRow(line, token.columns)
|
|
179
|
+
if (row == null) return { status: 'notMatched' }
|
|
180
|
+
|
|
181
|
+
token.rows.push(row)
|
|
182
|
+
return { status: 'opening', nextIndex: line.endIndex }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// process table row
|
|
186
|
+
function calcTableRow(line: IPhrasingContentLine, columns: TableColumn[]): ITableRowToken {
|
|
187
|
+
const { nodePoints, startIndex, endIndex, firstNonWhitespaceIndex } = line
|
|
188
|
+
|
|
189
|
+
// eat leading pipe
|
|
190
|
+
let p = nodePoints[firstNonWhitespaceIndex]
|
|
191
|
+
let i =
|
|
192
|
+
p.codePoint === AsciiCodePoint.VERTICAL_SLASH
|
|
193
|
+
? firstNonWhitespaceIndex + 1
|
|
194
|
+
: firstNonWhitespaceIndex
|
|
195
|
+
|
|
196
|
+
// eat table cells
|
|
197
|
+
const cells: ITableCellToken[] = []
|
|
198
|
+
for (; i < endIndex; i += 1) {
|
|
199
|
+
/**
|
|
200
|
+
* Spaces between pipes and cell content are trimmed
|
|
201
|
+
*/
|
|
202
|
+
for (; i < endIndex; ++i) {
|
|
203
|
+
p = nodePoints[i]
|
|
204
|
+
if (!isWhitespaceCharacter(p.codePoint)) break
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Start point of the table-cell.
|
|
208
|
+
const startPoint: Point =
|
|
209
|
+
i < endIndex ? calcStartPoint(nodePoints, i) : calcEndPoint(nodePoints, endIndex - 1)
|
|
210
|
+
|
|
211
|
+
// Eating cell contents.
|
|
212
|
+
const cellStartIndex = i,
|
|
213
|
+
cellFirstNonWhitespaceIndex = i
|
|
214
|
+
for (; i < endIndex; ++i) {
|
|
215
|
+
p = nodePoints[i]
|
|
216
|
+
/**
|
|
217
|
+
* Include a pipe in a cell’s content by escaping it,
|
|
218
|
+
* including inside other inline spans
|
|
219
|
+
*/
|
|
220
|
+
if (p.codePoint === AsciiCodePoint.BACKSLASH) {
|
|
221
|
+
i += 1
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// pipe are boundary character
|
|
226
|
+
if (p.codePoint === AsciiCodePoint.VERTICAL_SLASH) break
|
|
227
|
+
}
|
|
228
|
+
let cellEndIndex = i
|
|
229
|
+
for (; cellEndIndex > cellStartIndex; --cellEndIndex) {
|
|
230
|
+
const p = nodePoints[cellEndIndex - 1]
|
|
231
|
+
if (!isWhitespaceCharacter(p.codePoint)) break
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// End point of the table-cell
|
|
235
|
+
const endPoint: Point = calcEndPoint(nodePoints, i - 1)
|
|
236
|
+
|
|
237
|
+
const lines: IPhrasingContentLine[] =
|
|
238
|
+
cellFirstNonWhitespaceIndex >= cellEndIndex
|
|
239
|
+
? []
|
|
240
|
+
: [
|
|
241
|
+
{
|
|
242
|
+
nodePoints,
|
|
243
|
+
startIndex: cellStartIndex,
|
|
244
|
+
endIndex: cellEndIndex,
|
|
245
|
+
firstNonWhitespaceIndex: cellFirstNonWhitespaceIndex,
|
|
246
|
+
countOfPrecedeSpaces: cellFirstNonWhitespaceIndex - cellStartIndex,
|
|
247
|
+
},
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
const cell: ITableCellToken = {
|
|
251
|
+
nodeType: TableCellType,
|
|
252
|
+
position: { start: startPoint, end: endPoint },
|
|
253
|
+
lines: lines,
|
|
254
|
+
}
|
|
255
|
+
cells.push(cell)
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* If there are greater, the excess is ignored
|
|
259
|
+
* @see https://github.github.com/gfm/#example-204
|
|
260
|
+
*/
|
|
261
|
+
if (cells.length >= columns.length) break
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Start point of the table-row
|
|
265
|
+
const startPoint: Point = calcStartPoint(nodePoints, startIndex)
|
|
266
|
+
|
|
267
|
+
// End point of the table-row
|
|
268
|
+
const endPoint: Point = calcEndPoint(nodePoints, endIndex - 1)
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The remainder of the table’s rows may vary in the number of cells.
|
|
272
|
+
* If there are a number of cells fewer than the number of cells in
|
|
273
|
+
* the header row, empty cells are inserted. If there are greater,
|
|
274
|
+
* the excess is ignored
|
|
275
|
+
* @see https://github.github.com/gfm/#example-204
|
|
276
|
+
*/
|
|
277
|
+
for (let c = cells.length; c < columns.length; ++c) {
|
|
278
|
+
const cell: ITableCellToken = {
|
|
279
|
+
nodeType: TableCellType,
|
|
280
|
+
position: { start: { ...endPoint }, end: { ...endPoint } },
|
|
281
|
+
lines: [],
|
|
282
|
+
}
|
|
283
|
+
cells.push(cell)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const row: ITableRowToken = {
|
|
287
|
+
nodeType: TableRowType,
|
|
288
|
+
position: { start: startPoint, end: endPoint },
|
|
289
|
+
cells,
|
|
290
|
+
}
|
|
291
|
+
return row
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Find delimiter row
|
|
297
|
+
*
|
|
298
|
+
* The delimiter row consists of cells whose only content are
|
|
299
|
+
* hyphens (-), and optionally, a leading or trailing colon (:),
|
|
300
|
+
* or both, to indicate left, right, or center alignment respectively.
|
|
301
|
+
* @see https://github.github.com/gfm/#delimiter-row
|
|
302
|
+
*/
|
|
303
|
+
// function calcTableColumn(
|
|
304
|
+
// nodePoints: ReadonlyArray<INodePoint>,
|
|
305
|
+
// currentLine: IPhrasingContentLine,
|
|
306
|
+
// previousLine: IPhrasingContentLine,
|
|
307
|
+
// ): TableColumn[] | null {
|
|
308
|
+
// /**
|
|
309
|
+
// * The previous line of the delimiter line must not be blank line
|
|
310
|
+
// */
|
|
311
|
+
// if (previousLine.firstNonWhitespaceIndex >= previousLine.endIndex) {
|
|
312
|
+
// return null
|
|
313
|
+
// }
|
|
314
|
+
|
|
315
|
+
// /**
|
|
316
|
+
// * Four spaces is too much
|
|
317
|
+
// * @see https://github.github.com/gfm/#example-57
|
|
318
|
+
// */
|
|
319
|
+
// if (currentLine.firstNonWhitespaceIndex - currentLine.startIndex >= 4) return null
|
|
320
|
+
|
|
321
|
+
// const columns: TableColumn[] = []
|
|
322
|
+
|
|
323
|
+
// /**
|
|
324
|
+
// * eat leading optional pipe
|
|
325
|
+
// */
|
|
326
|
+
// let p = nodePoints[currentLine.firstNonWhitespaceIndex]
|
|
327
|
+
// let cIndex =
|
|
328
|
+
// p.codePoint === AsciiCodePoint.VERTICAL_SLASH
|
|
329
|
+
// ? currentLine.firstNonWhitespaceIndex + 1
|
|
330
|
+
// : currentLine.firstNonWhitespaceIndex
|
|
331
|
+
|
|
332
|
+
// for (; cIndex < currentLine.endIndex; ) {
|
|
333
|
+
// for (; cIndex < currentLine.endIndex; ++cIndex) {
|
|
334
|
+
// p = nodePoints[cIndex]
|
|
335
|
+
// if (!isWhitespaceCharacter(p.codePoint)) break
|
|
336
|
+
// }
|
|
337
|
+
// if (cIndex >= currentLine.endIndex) break
|
|
338
|
+
|
|
339
|
+
// // eat left optional colon
|
|
340
|
+
// let leftColon = false
|
|
341
|
+
// if (p.codePoint === AsciiCodePoint.COLON) {
|
|
342
|
+
// leftColon = true
|
|
343
|
+
// cIndex += 1
|
|
344
|
+
// }
|
|
345
|
+
|
|
346
|
+
// let hyphenCount = 0
|
|
347
|
+
// for (; cIndex < currentLine.endIndex; ++cIndex) {
|
|
348
|
+
// p = nodePoints[cIndex]
|
|
349
|
+
// if (p.codePoint !== AsciiCodePoint.MINUS_SIGN) break
|
|
350
|
+
// hyphenCount += 1
|
|
351
|
+
// }
|
|
352
|
+
|
|
353
|
+
// // hyphen must be exist
|
|
354
|
+
// if (hyphenCount <= 0) return null
|
|
355
|
+
|
|
356
|
+
// // eat right optional colon
|
|
357
|
+
// let rightColon = false
|
|
358
|
+
// if (cIndex < currentLine.endIndex && p.codePoint === AsciiCodePoint.COLON) {
|
|
359
|
+
// rightColon = true
|
|
360
|
+
// cIndex += 1
|
|
361
|
+
// }
|
|
362
|
+
|
|
363
|
+
// // eating next pipe
|
|
364
|
+
// for (; cIndex < currentLine.endIndex; ++cIndex) {
|
|
365
|
+
// p = nodePoints[cIndex]
|
|
366
|
+
// if (isWhitespaceCharacter(p.codePoint)) continue
|
|
367
|
+
// if (p.codePoint === AsciiCodePoint.VERTICAL_SLASH) {
|
|
368
|
+
// cIndex += 1
|
|
369
|
+
// break
|
|
370
|
+
// }
|
|
371
|
+
|
|
372
|
+
// // There are other non-white space characters
|
|
373
|
+
// return null
|
|
374
|
+
// }
|
|
375
|
+
|
|
376
|
+
// let align: AlignType = null
|
|
377
|
+
// if (leftColon && rightColon) align = 'center'
|
|
378
|
+
// else if (leftColon) align = 'left'
|
|
379
|
+
// else if (rightColon) align = 'right'
|
|
380
|
+
// const column: TableColumn = { align }
|
|
381
|
+
// columns.push(column)
|
|
382
|
+
// }
|
|
383
|
+
|
|
384
|
+
// if (columns.length <= 0) return null
|
|
385
|
+
|
|
386
|
+
// /**
|
|
387
|
+
// * The header row must match the delimiter row in the number of cells.
|
|
388
|
+
// * If not, a table will not be recognized
|
|
389
|
+
// * @see https://github.github.com/gfm/#example-203
|
|
390
|
+
// */
|
|
391
|
+
// let cellCount = 0,
|
|
392
|
+
// hasNonWhitespaceBeforePipe = false
|
|
393
|
+
// for (let pIndex = previousLine.startIndex; pIndex < previousLine.endIndex; ++pIndex) {
|
|
394
|
+
// const p = nodePoints[pIndex]
|
|
395
|
+
// if (isWhitespaceCharacter(p.codePoint)) continue
|
|
396
|
+
|
|
397
|
+
// if (p.codePoint === AsciiCodePoint.VERTICAL_SLASH) {
|
|
398
|
+
// if (hasNonWhitespaceBeforePipe || cellCount > 0) cellCount += 1
|
|
399
|
+
// hasNonWhitespaceBeforePipe = false
|
|
400
|
+
// continue
|
|
401
|
+
// }
|
|
402
|
+
|
|
403
|
+
// hasNonWhitespaceBeforePipe = true
|
|
404
|
+
|
|
405
|
+
// /**
|
|
406
|
+
// * Include a pipe in a cell’s content by escaping it,
|
|
407
|
+
// * including inside other inline spans
|
|
408
|
+
// */
|
|
409
|
+
// if (p.codePoint === AsciiCodePoint.BACKSLASH) pIndex += 1
|
|
410
|
+
// }
|
|
411
|
+
// if (hasNonWhitespaceBeforePipe && columns.length > 1) cellCount += 1
|
|
412
|
+
// if (cellCount !== columns.length) return null
|
|
413
|
+
|
|
414
|
+
// // Successfully matched to a legal table delimiter line
|
|
415
|
+
// return columns
|
|
416
|
+
// }
|
package/src/parse.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Node, TableCell, TableRow } from '@yozora/ast'
|
|
2
|
+
import { TableCellType, TableRowType, TableType } from '@yozora/ast'
|
|
3
|
+
import type { INodePoint } from '@yozora/character'
|
|
4
|
+
import { AsciiCodePoint } from '@yozora/character'
|
|
5
|
+
import type { IParseBlockHookCreator } from '@yozora/core-tokenizer'
|
|
6
|
+
import { mergeAndStripContentLines } from '@yozora/core-tokenizer'
|
|
7
|
+
import type { INode, IThis, IToken, T } from './types'
|
|
8
|
+
|
|
9
|
+
export const parse: IParseBlockHookCreator<T, IToken, INode, IThis> = api => ({
|
|
10
|
+
parse: tokens =>
|
|
11
|
+
tokens.map(token => {
|
|
12
|
+
const tableRows: TableRow[] = token.rows.map((row): TableRow => {
|
|
13
|
+
const tableCells: TableCell[] = row.cells.map((cell): TableCell => {
|
|
14
|
+
/**
|
|
15
|
+
* Include a pipe in a cell’s content by escaping it, including inside
|
|
16
|
+
* other inline spans
|
|
17
|
+
* @see https://github.github.com/gfm/#example-200
|
|
18
|
+
*/
|
|
19
|
+
const contents: INodePoint[] = []
|
|
20
|
+
{
|
|
21
|
+
const nodePoints: INodePoint[] = mergeAndStripContentLines(cell.lines)
|
|
22
|
+
for (let i = 0, endIndex = nodePoints.length; i < endIndex; ++i) {
|
|
23
|
+
const p = nodePoints[i]
|
|
24
|
+
if (p.codePoint === AsciiCodePoint.BACKSLASH && i + 1 < endIndex) {
|
|
25
|
+
const q: INodePoint = nodePoints[i + 1]
|
|
26
|
+
if (q.codePoint !== AsciiCodePoint.VERTICAL_SLASH) contents.push(p)
|
|
27
|
+
contents.push(q)
|
|
28
|
+
i += 1
|
|
29
|
+
} else {
|
|
30
|
+
contents.push(p)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const children: Node[] = api.processInlines(contents)
|
|
36
|
+
const tableCell: TableCell = api.shouldReservePosition
|
|
37
|
+
? { type: TableCellType, position: cell.position, children }
|
|
38
|
+
: { type: TableCellType, children }
|
|
39
|
+
return tableCell
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const tableRow: TableRow = api.shouldReservePosition
|
|
43
|
+
? { type: TableRowType, position: row.position, children: tableCells }
|
|
44
|
+
: { type: TableRowType, children: tableCells }
|
|
45
|
+
return tableRow
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const table: INode = api.shouldReservePosition
|
|
49
|
+
? { type: TableType, position: token.position, columns: token.columns, children: tableRows }
|
|
50
|
+
: { type: TableType, columns: token.columns, children: tableRows }
|
|
51
|
+
return table
|
|
52
|
+
}),
|
|
53
|
+
})
|
package/src/tokenizer.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IBlockTokenizer,
|
|
3
|
+
IMatchBlockHookCreator,
|
|
4
|
+
IParseBlockHookCreator,
|
|
5
|
+
} from '@yozora/core-tokenizer'
|
|
6
|
+
import { BaseBlockTokenizer, TokenizerPriority } from '@yozora/core-tokenizer'
|
|
7
|
+
import { match } from './match'
|
|
8
|
+
import { parse } from './parse'
|
|
9
|
+
import type { INode, IThis, IToken, ITokenizerProps, T } from './types'
|
|
10
|
+
import { uniqueName } from './types'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lexical Analyzer for Table, table-row and table-cell.
|
|
14
|
+
* @see https://github.github.com/gfm/#table
|
|
15
|
+
* @see https://github.com/syntax-tree/mdast#tablerow
|
|
16
|
+
* @see https://github.com/syntax-tree/mdast#tablecell
|
|
17
|
+
*/
|
|
18
|
+
export class TableTokenizer
|
|
19
|
+
extends BaseBlockTokenizer<T, IToken, INode, IThis>
|
|
20
|
+
implements IBlockTokenizer<T, IToken, INode, IThis>
|
|
21
|
+
{
|
|
22
|
+
/* istanbul ignore next */
|
|
23
|
+
constructor(props: ITokenizerProps = {}) {
|
|
24
|
+
super({
|
|
25
|
+
name: props.name ?? uniqueName,
|
|
26
|
+
priority: props.priority ?? TokenizerPriority.INTERRUPTABLE_BLOCK,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public override readonly match: IMatchBlockHookCreator<T, IToken, IThis> = match
|
|
31
|
+
|
|
32
|
+
public override readonly parse: IParseBlockHookCreator<T, IToken, INode, IThis> = parse
|
|
33
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Position,
|
|
3
|
+
Table,
|
|
4
|
+
TableCellType,
|
|
5
|
+
TableColumn,
|
|
6
|
+
TableRowType,
|
|
7
|
+
TableType,
|
|
8
|
+
} from '@yozora/ast'
|
|
9
|
+
import type {
|
|
10
|
+
IBaseBlockTokenizerProps,
|
|
11
|
+
IPartialYastBlockToken,
|
|
12
|
+
IPhrasingContentLine,
|
|
13
|
+
ITokenizer,
|
|
14
|
+
} from '@yozora/core-tokenizer'
|
|
15
|
+
|
|
16
|
+
export type T = TableType
|
|
17
|
+
export type INode = Table
|
|
18
|
+
export const uniqueName = '@yozora/tokenizer-table'
|
|
19
|
+
|
|
20
|
+
export interface IToken extends IPartialYastBlockToken<TableType> {
|
|
21
|
+
/**
|
|
22
|
+
* Table column configuration items
|
|
23
|
+
*/
|
|
24
|
+
columns: TableColumn[]
|
|
25
|
+
/**
|
|
26
|
+
* Table rows
|
|
27
|
+
*/
|
|
28
|
+
rows: ITableRowToken[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type IThis = ITokenizer
|
|
32
|
+
|
|
33
|
+
export type ITokenizerProps = Partial<IBaseBlockTokenizerProps>
|
|
34
|
+
|
|
35
|
+
export interface ITableRowToken extends IPartialYastBlockToken<TableRowType> {
|
|
36
|
+
cells: ITableCellToken[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ITableCellToken extends IPartialYastBlockToken<TableCellType> {
|
|
40
|
+
position: Position
|
|
41
|
+
lines: IPhrasingContentLine[]
|
|
42
|
+
}
|