@yozora/tokenizer-link 2.1.3 → 2.1.4
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/package.json +6 -6
- package/src/index.ts +0 -10
- package/src/match.ts +0 -207
- package/src/parse.ts +0 -46
- package/src/tokenizer.ts +0 -32
- package/src/types.ts +0 -42
- package/src/util/check-brackets.ts +0 -54
- package/src/util/link-destination.ts +0 -85
- package/src/util/link-title.ts +0 -82
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yozora/tokenizer-link",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "guanghechen",
|
|
6
6
|
"url": "https://github.com/guanghechen/"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"lib/",
|
|
30
|
-
"
|
|
30
|
+
"lib/**/*.map",
|
|
31
31
|
"package.json",
|
|
32
32
|
"CHANGELOG.md",
|
|
33
33
|
"LICENSE",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"test": "cross-env TS_NODE_FILES=true NODE_OPTIONS=--experimental-vm-modules jest --config ../../jest.config.mjs --rootDir ."
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@yozora/ast": "^2.1.
|
|
43
|
-
"@yozora/character": "^2.1.
|
|
44
|
-
"@yozora/core-tokenizer": "^2.1.
|
|
42
|
+
"@yozora/ast": "^2.1.4",
|
|
43
|
+
"@yozora/character": "^2.1.4",
|
|
44
|
+
"@yozora/core-tokenizer": "^2.1.4"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "aa464ed1e3cd84892773a833910cfc53a556bf5f"
|
|
47
47
|
}
|
package/src/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export * from './util/check-brackets'
|
|
2
|
-
export * from './util/link-destination'
|
|
3
|
-
export * from './util/link-title'
|
|
4
|
-
export { LinkTokenizer, LinkTokenizer as default } from './tokenizer'
|
|
5
|
-
export { uniqueName as LinkTokenizerName } from './types'
|
|
6
|
-
export type {
|
|
7
|
-
IThis as ILinkHookContext,
|
|
8
|
-
IToken as ILinkToken,
|
|
9
|
-
ITokenizerProps as ILinkTokenizerProps,
|
|
10
|
-
} from './types'
|
package/src/match.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { LinkType } from '@yozora/ast'
|
|
2
|
-
import type { INodePoint } from '@yozora/character'
|
|
3
|
-
import { AsciiCodePoint } from '@yozora/character'
|
|
4
|
-
import type {
|
|
5
|
-
IInlineToken,
|
|
6
|
-
IMatchInlineHookCreator,
|
|
7
|
-
IResultOfIsDelimiterPair,
|
|
8
|
-
IResultOfProcessDelimiterPair,
|
|
9
|
-
} from '@yozora/core-tokenizer'
|
|
10
|
-
import { eatOptionalWhitespaces, genFindDelimiter, isLinkToken } from '@yozora/core-tokenizer'
|
|
11
|
-
import type { IDelimiter, IThis, IToken, T } from './types'
|
|
12
|
-
import { checkBalancedBracketsStatus } from './util/check-brackets'
|
|
13
|
-
import { eatLinkDestination } from './util/link-destination'
|
|
14
|
-
import { eatLinkTitle } from './util/link-title'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* An inline link consists of a link text followed immediately by a left
|
|
18
|
-
* parenthesis '(', optional whitespace, an optional link destination, an
|
|
19
|
-
* optional link title separated from the link destination by whitespace,
|
|
20
|
-
* optional whitespace, and a right parenthesis ')'. The link’s text consists
|
|
21
|
-
* of the inlines contained in the link text (excluding the enclosing square
|
|
22
|
-
* brackets).
|
|
23
|
-
* The link’s URI consists of the link destination, excluding enclosing '<...>'
|
|
24
|
-
* if present, with backslash-escapes in effect as described above. The link’s
|
|
25
|
-
* title consists of the link title, excluding its enclosing delimiters, with
|
|
26
|
-
* backslash-escapes in effect as described above.
|
|
27
|
-
*
|
|
28
|
-
* ------
|
|
29
|
-
*
|
|
30
|
-
* A 'opener' type delimiter is one of the following forms:
|
|
31
|
-
*
|
|
32
|
-
* - '['
|
|
33
|
-
*
|
|
34
|
-
* A 'closer' type delimiter is one of the following forms:
|
|
35
|
-
*
|
|
36
|
-
* - '](url)'
|
|
37
|
-
* - '](url "title")'
|
|
38
|
-
* - '](<url>)'
|
|
39
|
-
* - '](<url> "title")'
|
|
40
|
-
*
|
|
41
|
-
* @see https://github.com/syntax-tree/mdast#link
|
|
42
|
-
* @see https://github.github.com/gfm/#links
|
|
43
|
-
*/
|
|
44
|
-
export const match: IMatchInlineHookCreator<T, IDelimiter, IToken, IThis> = function (api) {
|
|
45
|
-
return {
|
|
46
|
-
findDelimiter: () => genFindDelimiter<IDelimiter>(_findDelimiter),
|
|
47
|
-
isDelimiterPair,
|
|
48
|
-
processDelimiterPair,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* An inline link consists of a link text followed immediately by a left
|
|
53
|
-
* parenthesis '(', optional whitespace, an optional link destination, an
|
|
54
|
-
* optional link title separated from the link destination by whitespace,
|
|
55
|
-
* optional whitespace, and a right parenthesis ')'
|
|
56
|
-
* @see https://github.github.com/gfm/#inline-link
|
|
57
|
-
*/
|
|
58
|
-
function _findDelimiter(startIndex: number, endIndex: number): IDelimiter | null {
|
|
59
|
-
const nodePoints: ReadonlyArray<INodePoint> = api.getNodePoints()
|
|
60
|
-
const blockEndIndex = api.getBlockEndIndex()
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* FIXME:
|
|
64
|
-
*
|
|
65
|
-
* This is a hack method to fix the situation where a higher priority token
|
|
66
|
-
* is embedded in the delimiter, at this time, ignore the tokens that have
|
|
67
|
-
* been parsed, and continue to match the content until the delimiter meets
|
|
68
|
-
* its own definition or reaches the right boundary of the block content.
|
|
69
|
-
*
|
|
70
|
-
* This algorithm has not been strictly logically verified, but I think it
|
|
71
|
-
* can work well in most cases. After all, it has passed many test cases.
|
|
72
|
-
* @see https://github.github.com/gfm/#example-588
|
|
73
|
-
*/
|
|
74
|
-
for (let i = startIndex; i < endIndex; ++i) {
|
|
75
|
-
const p = nodePoints[i]
|
|
76
|
-
switch (p.codePoint) {
|
|
77
|
-
case AsciiCodePoint.BACKSLASH:
|
|
78
|
-
i += 1
|
|
79
|
-
break
|
|
80
|
-
/**
|
|
81
|
-
* A link text consists of a sequence of zero or more inline elements
|
|
82
|
-
* enclosed by square brackets ([ and ])
|
|
83
|
-
* @see https://github.github.com/gfm/#link-text
|
|
84
|
-
*/
|
|
85
|
-
case AsciiCodePoint.OPEN_BRACKET: {
|
|
86
|
-
const delimiter: IDelimiter = {
|
|
87
|
-
type: 'opener',
|
|
88
|
-
startIndex: i,
|
|
89
|
-
endIndex: i + 1,
|
|
90
|
-
}
|
|
91
|
-
return delimiter
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* An inline link consists of a link text followed immediately by a
|
|
95
|
-
* left parenthesis '(', ..., and a right parenthesis ')'
|
|
96
|
-
* @see https://github.github.com/gfm/#inline-link
|
|
97
|
-
*/
|
|
98
|
-
case AsciiCodePoint.CLOSE_BRACKET: {
|
|
99
|
-
if (i + 1 >= endIndex || nodePoints[i + 1].codePoint !== AsciiCodePoint.OPEN_PARENTHESIS)
|
|
100
|
-
break
|
|
101
|
-
|
|
102
|
-
// try to match link destination
|
|
103
|
-
const destinationStartIndex = eatOptionalWhitespaces(nodePoints, i + 2, blockEndIndex)
|
|
104
|
-
const destinationEndIndex = eatLinkDestination(
|
|
105
|
-
nodePoints,
|
|
106
|
-
destinationStartIndex,
|
|
107
|
-
blockEndIndex,
|
|
108
|
-
)
|
|
109
|
-
if (destinationEndIndex < 0) break // no valid destination matched
|
|
110
|
-
|
|
111
|
-
// try to match link title
|
|
112
|
-
const titleStartIndex = eatOptionalWhitespaces(
|
|
113
|
-
nodePoints,
|
|
114
|
-
destinationEndIndex,
|
|
115
|
-
blockEndIndex,
|
|
116
|
-
)
|
|
117
|
-
const titleEndIndex = eatLinkTitle(nodePoints, titleStartIndex, blockEndIndex)
|
|
118
|
-
if (titleEndIndex < 0) break
|
|
119
|
-
|
|
120
|
-
const _startIndex = i
|
|
121
|
-
const _endIndex = eatOptionalWhitespaces(nodePoints, titleEndIndex, blockEndIndex) + 1
|
|
122
|
-
if (
|
|
123
|
-
_endIndex > blockEndIndex ||
|
|
124
|
-
nodePoints[_endIndex - 1].codePoint !== AsciiCodePoint.CLOSE_PARENTHESIS
|
|
125
|
-
)
|
|
126
|
-
break
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Both the title and the destination may be omitted
|
|
130
|
-
* @see https://github.github.com/gfm/#example-495
|
|
131
|
-
*/
|
|
132
|
-
return {
|
|
133
|
-
type: 'closer',
|
|
134
|
-
startIndex: _startIndex,
|
|
135
|
-
endIndex: _endIndex,
|
|
136
|
-
destinationContent:
|
|
137
|
-
destinationStartIndex < destinationEndIndex
|
|
138
|
-
? {
|
|
139
|
-
startIndex: destinationStartIndex,
|
|
140
|
-
endIndex: destinationEndIndex,
|
|
141
|
-
}
|
|
142
|
-
: undefined,
|
|
143
|
-
titleContent:
|
|
144
|
-
titleStartIndex < titleEndIndex
|
|
145
|
-
? { startIndex: titleStartIndex, endIndex: titleEndIndex }
|
|
146
|
-
: undefined,
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return null
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function isDelimiterPair(
|
|
155
|
-
openerDelimiter: IDelimiter,
|
|
156
|
-
closerDelimiter: IDelimiter,
|
|
157
|
-
internalTokens: ReadonlyArray<IInlineToken>,
|
|
158
|
-
): IResultOfIsDelimiterPair {
|
|
159
|
-
const nodePoints: ReadonlyArray<INodePoint> = api.getNodePoints()
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Links may not contain other links, at any level of nesting.
|
|
163
|
-
* @see https://github.github.com/gfm/#example-540
|
|
164
|
-
* @see https://github.github.com/gfm/#example-541
|
|
165
|
-
*/
|
|
166
|
-
const hasInternalLinkToken: boolean = internalTokens.find(isLinkToken) != null
|
|
167
|
-
if (hasInternalLinkToken) {
|
|
168
|
-
return { paired: false, opener: false, closer: false }
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const balancedBracketsStatus: -1 | 0 | 1 = checkBalancedBracketsStatus(
|
|
172
|
-
openerDelimiter.endIndex,
|
|
173
|
-
closerDelimiter.startIndex,
|
|
174
|
-
internalTokens,
|
|
175
|
-
nodePoints,
|
|
176
|
-
)
|
|
177
|
-
switch (balancedBracketsStatus) {
|
|
178
|
-
case -1:
|
|
179
|
-
return { paired: false, opener: false, closer: true }
|
|
180
|
-
case 0:
|
|
181
|
-
return { paired: true }
|
|
182
|
-
case 1:
|
|
183
|
-
return { paired: false, opener: true, closer: false }
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function processDelimiterPair(
|
|
188
|
-
openerDelimiter: IDelimiter,
|
|
189
|
-
closerDelimiter: IDelimiter,
|
|
190
|
-
internalTokens: ReadonlyArray<IInlineToken>,
|
|
191
|
-
): IResultOfProcessDelimiterPair<T, IToken, IDelimiter> {
|
|
192
|
-
const children: ReadonlyArray<IInlineToken> = api.resolveInternalTokens(
|
|
193
|
-
internalTokens,
|
|
194
|
-
openerDelimiter.endIndex,
|
|
195
|
-
closerDelimiter.startIndex,
|
|
196
|
-
)
|
|
197
|
-
const token: IToken = {
|
|
198
|
-
nodeType: LinkType,
|
|
199
|
-
startIndex: openerDelimiter.startIndex,
|
|
200
|
-
endIndex: closerDelimiter.endIndex,
|
|
201
|
-
destinationContent: closerDelimiter.destinationContent,
|
|
202
|
-
titleContent: closerDelimiter.titleContent,
|
|
203
|
-
children,
|
|
204
|
-
}
|
|
205
|
-
return { tokens: [token] }
|
|
206
|
-
}
|
|
207
|
-
}
|
package/src/parse.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { Node } from '@yozora/ast'
|
|
2
|
-
import { LinkType } from '@yozora/ast'
|
|
3
|
-
import type { INodePoint } from '@yozora/character'
|
|
4
|
-
import { AsciiCodePoint, calcEscapedStringFromNodePoints } from '@yozora/character'
|
|
5
|
-
import type { IParseInlineHookCreator } from '@yozora/core-tokenizer'
|
|
6
|
-
import { encodeLinkDestination } from '@yozora/core-tokenizer'
|
|
7
|
-
import type { INode, IThis, IToken, T } from './types'
|
|
8
|
-
|
|
9
|
-
export const parse: IParseInlineHookCreator<T, IToken, INode, IThis> = function (api) {
|
|
10
|
-
return {
|
|
11
|
-
parse: tokens =>
|
|
12
|
-
tokens.map(token => {
|
|
13
|
-
const nodePoints: ReadonlyArray<INodePoint> = api.getNodePoints()
|
|
14
|
-
|
|
15
|
-
// calc url
|
|
16
|
-
let url = ''
|
|
17
|
-
if (token.destinationContent != null) {
|
|
18
|
-
let { startIndex, endIndex } = token.destinationContent
|
|
19
|
-
if (nodePoints[startIndex].codePoint === AsciiCodePoint.OPEN_ANGLE) {
|
|
20
|
-
startIndex += 1
|
|
21
|
-
endIndex -= 1
|
|
22
|
-
}
|
|
23
|
-
const destination = calcEscapedStringFromNodePoints(
|
|
24
|
-
nodePoints,
|
|
25
|
-
startIndex,
|
|
26
|
-
endIndex,
|
|
27
|
-
true,
|
|
28
|
-
)
|
|
29
|
-
url = encodeLinkDestination(destination)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// calc title
|
|
33
|
-
let title: string | undefined
|
|
34
|
-
if (token.titleContent != null) {
|
|
35
|
-
const { startIndex, endIndex } = token.titleContent
|
|
36
|
-
title = calcEscapedStringFromNodePoints(nodePoints, startIndex + 1, endIndex - 1)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const children: Node[] = api.parseInlineTokens(token.children)
|
|
40
|
-
const node: INode = api.shouldReservePosition
|
|
41
|
-
? { type: LinkType, position: api.calcPosition(token), url, title, children }
|
|
42
|
-
: { type: LinkType, url, title, children }
|
|
43
|
-
return node
|
|
44
|
-
}),
|
|
45
|
-
}
|
|
46
|
-
}
|
package/src/tokenizer.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IInlineTokenizer,
|
|
3
|
-
IMatchInlineHookCreator,
|
|
4
|
-
IParseInlineHookCreator,
|
|
5
|
-
} from '@yozora/core-tokenizer'
|
|
6
|
-
import { BaseInlineTokenizer, TokenizerPriority } from '@yozora/core-tokenizer'
|
|
7
|
-
import { match } from './match'
|
|
8
|
-
import { parse } from './parse'
|
|
9
|
-
import type { IDelimiter, INode, IThis, IToken, ITokenizerProps, T } from './types'
|
|
10
|
-
import { uniqueName } from './types'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Lexical Analyzer for InlineLink.
|
|
14
|
-
* @see https://github.com/syntax-tree/mdast#link
|
|
15
|
-
* @see https://github.github.com/gfm/#links
|
|
16
|
-
*/
|
|
17
|
-
export class LinkTokenizer
|
|
18
|
-
extends BaseInlineTokenizer<T, IDelimiter, IToken, INode, IThis>
|
|
19
|
-
implements IInlineTokenizer<T, IDelimiter, IToken, INode, IThis>
|
|
20
|
-
{
|
|
21
|
-
/* istanbul ignore next */
|
|
22
|
-
constructor(props: ITokenizerProps = {}) {
|
|
23
|
-
super({
|
|
24
|
-
name: props.name ?? uniqueName,
|
|
25
|
-
priority: props.priority ?? TokenizerPriority.LINKS,
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
public override readonly match: IMatchInlineHookCreator<T, IDelimiter, IToken, IThis> = match
|
|
30
|
-
|
|
31
|
-
public override readonly parse: IParseInlineHookCreator<T, IToken, INode, IThis> = parse
|
|
32
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { Link, LinkType } from '@yozora/ast'
|
|
2
|
-
import type { INodeInterval } from '@yozora/character'
|
|
3
|
-
import type {
|
|
4
|
-
IBaseInlineTokenizerProps,
|
|
5
|
-
IPartialInlineToken,
|
|
6
|
-
ITokenDelimiter,
|
|
7
|
-
ITokenizer,
|
|
8
|
-
} from '@yozora/core-tokenizer'
|
|
9
|
-
|
|
10
|
-
export type T = LinkType
|
|
11
|
-
export type INode = Link
|
|
12
|
-
export const uniqueName = '@yozora/tokenizer-link'
|
|
13
|
-
|
|
14
|
-
export interface IToken extends IPartialInlineToken<T> {
|
|
15
|
-
/**
|
|
16
|
-
* Link destination interval.
|
|
17
|
-
*/
|
|
18
|
-
destinationContent?: INodeInterval
|
|
19
|
-
/**
|
|
20
|
-
* Link title interval.
|
|
21
|
-
*/
|
|
22
|
-
titleContent?: INodeInterval
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface IDelimiter extends ITokenDelimiter {
|
|
26
|
-
/**
|
|
27
|
-
* IDelimiter type.
|
|
28
|
-
*/
|
|
29
|
-
type: 'opener' | 'closer'
|
|
30
|
-
/**
|
|
31
|
-
* Link destination interval.
|
|
32
|
-
*/
|
|
33
|
-
destinationContent?: INodeInterval
|
|
34
|
-
/**
|
|
35
|
-
* Link title interval.
|
|
36
|
-
*/
|
|
37
|
-
titleContent?: INodeInterval
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type IThis = ITokenizer
|
|
41
|
-
|
|
42
|
-
export type ITokenizerProps = Partial<IBaseInlineTokenizerProps>
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { INodePoint } from '@yozora/character'
|
|
2
|
-
import { AsciiCodePoint } from '@yozora/character'
|
|
3
|
-
import type { IInlineToken } from '@yozora/core-tokenizer'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The link text may contain balanced brackets, but not unbalanced ones,
|
|
7
|
-
* unless they are escaped
|
|
8
|
-
*
|
|
9
|
-
* @see https://github.github.com/gfm/#example-520
|
|
10
|
-
* @see https://github.github.com/gfm/#example-521
|
|
11
|
-
* @see https://github.github.com/gfm/#example-522
|
|
12
|
-
* @see https://github.github.com/gfm/#example-523
|
|
13
|
-
*/
|
|
14
|
-
export const checkBalancedBracketsStatus = (
|
|
15
|
-
startIndex: number,
|
|
16
|
-
endIndex: number,
|
|
17
|
-
internalTokens: ReadonlyArray<IInlineToken>,
|
|
18
|
-
nodePoints: ReadonlyArray<INodePoint>,
|
|
19
|
-
): -1 | 0 | 1 => {
|
|
20
|
-
let i = startIndex
|
|
21
|
-
let bracketCount = 0
|
|
22
|
-
|
|
23
|
-
// update bracketCount
|
|
24
|
-
const updateBracketCount = (): void => {
|
|
25
|
-
const c = nodePoints[i].codePoint
|
|
26
|
-
switch (c) {
|
|
27
|
-
case AsciiCodePoint.BACKSLASH:
|
|
28
|
-
i += 1
|
|
29
|
-
break
|
|
30
|
-
case AsciiCodePoint.OPEN_BRACKET:
|
|
31
|
-
bracketCount += 1
|
|
32
|
-
break
|
|
33
|
-
case AsciiCodePoint.CLOSE_BRACKET:
|
|
34
|
-
bracketCount -= 1
|
|
35
|
-
break
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
for (const token of internalTokens) {
|
|
40
|
-
if (token.startIndex < startIndex) continue
|
|
41
|
-
if (token.endIndex > endIndex) break
|
|
42
|
-
for (; i < token.startIndex; ++i) {
|
|
43
|
-
updateBracketCount()
|
|
44
|
-
if (bracketCount < 0) return -1
|
|
45
|
-
}
|
|
46
|
-
i = token.endIndex
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
for (; i < endIndex; ++i) {
|
|
50
|
-
updateBracketCount()
|
|
51
|
-
if (bracketCount < 0) return -1
|
|
52
|
-
}
|
|
53
|
-
return bracketCount > 0 ? 1 : 0
|
|
54
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { INodePoint } from '@yozora/character'
|
|
2
|
-
import {
|
|
3
|
-
AsciiCodePoint,
|
|
4
|
-
VirtualCodePoint,
|
|
5
|
-
isAsciiControlCharacter,
|
|
6
|
-
isWhitespaceCharacter,
|
|
7
|
-
} from '@yozora/character'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A link destination consists of either
|
|
11
|
-
* - a sequence of zero or more characters between an opening '<' and a closing '>'
|
|
12
|
-
* that contains no line breaks or unescaped '<' or '>' characters, or
|
|
13
|
-
* - a nonempty sequence of characters that does not start with '<', does not include
|
|
14
|
-
* ASCII space or control characters, and includes parentheses only if
|
|
15
|
-
* (a) they are backslash-escaped or
|
|
16
|
-
* (b) they are part of a balanced pair of unescaped parentheses. (Implementations
|
|
17
|
-
* may impose limits on parentheses nesting to avoid performance issues, but
|
|
18
|
-
* at least three levels of nesting should be supported.)
|
|
19
|
-
* @see https://github.github.com/gfm/#link-destination
|
|
20
|
-
* @return position at next iteration
|
|
21
|
-
*/
|
|
22
|
-
export function eatLinkDestination(
|
|
23
|
-
nodePoints: ReadonlyArray<INodePoint>,
|
|
24
|
-
startIndex: number,
|
|
25
|
-
endIndex: number,
|
|
26
|
-
): number {
|
|
27
|
-
if (startIndex >= endIndex) return -1
|
|
28
|
-
|
|
29
|
-
let i = startIndex
|
|
30
|
-
switch (nodePoints[i].codePoint) {
|
|
31
|
-
/**
|
|
32
|
-
* In pointy brackets:
|
|
33
|
-
* - A sequence of zero or more characters between an opening '<' and
|
|
34
|
-
* a closing '>' that contains no line breaks or unescaped '<' or '>' characters
|
|
35
|
-
*/
|
|
36
|
-
case AsciiCodePoint.OPEN_ANGLE: {
|
|
37
|
-
for (i += 1; i < endIndex; ++i) {
|
|
38
|
-
const p = nodePoints[i]
|
|
39
|
-
switch (p.codePoint) {
|
|
40
|
-
case AsciiCodePoint.BACKSLASH:
|
|
41
|
-
i += 1
|
|
42
|
-
break
|
|
43
|
-
case AsciiCodePoint.OPEN_ANGLE:
|
|
44
|
-
case VirtualCodePoint.LINE_END:
|
|
45
|
-
return -1
|
|
46
|
-
case AsciiCodePoint.CLOSE_ANGLE:
|
|
47
|
-
return i + 1
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return -1
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Not in pointy brackets:
|
|
54
|
-
* - A nonempty sequence of characters that does not start with '<', does not include
|
|
55
|
-
* ASCII space or control characters, and includes parentheses only if
|
|
56
|
-
*
|
|
57
|
-
* a) they are backslash-escaped or
|
|
58
|
-
* b) they are part of a balanced pair of unescaped parentheses. (Implementations
|
|
59
|
-
* may impose limits on parentheses nesting to avoid performance issues,
|
|
60
|
-
* but at least three levels of nesting should be supported.)
|
|
61
|
-
*/
|
|
62
|
-
default: {
|
|
63
|
-
let openParensCount = 0
|
|
64
|
-
for (; i < endIndex; ++i) {
|
|
65
|
-
const c = nodePoints[i].codePoint
|
|
66
|
-
switch (c) {
|
|
67
|
-
case AsciiCodePoint.BACKSLASH:
|
|
68
|
-
i += 1
|
|
69
|
-
break
|
|
70
|
-
case AsciiCodePoint.OPEN_PARENTHESIS:
|
|
71
|
-
openParensCount += 1
|
|
72
|
-
break
|
|
73
|
-
case AsciiCodePoint.CLOSE_PARENTHESIS:
|
|
74
|
-
openParensCount -= 1
|
|
75
|
-
if (openParensCount < 0) return i
|
|
76
|
-
break
|
|
77
|
-
default:
|
|
78
|
-
if (isWhitespaceCharacter(c) || isAsciiControlCharacter(c)) return i
|
|
79
|
-
break
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return openParensCount === 0 ? i : -1
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
package/src/util/link-title.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type { INodePoint } from '@yozora/character'
|
|
2
|
-
import { AsciiCodePoint, VirtualCodePoint } from '@yozora/character'
|
|
3
|
-
import { eatOptionalBlankLines } from '@yozora/core-tokenizer'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A link title consists of either
|
|
7
|
-
*
|
|
8
|
-
* - a sequence of zero or more characters between straight double-quote
|
|
9
|
-
* characters '"', including a '"' character only if it is backslash-escaped, or
|
|
10
|
-
*
|
|
11
|
-
* - a sequence of zero or more characters between straight single-quote
|
|
12
|
-
* characters '\'', including a '\'' character only if it is backslash-escaped, or
|
|
13
|
-
*
|
|
14
|
-
* - a sequence of zero or more characters between matching parentheses '(...)',
|
|
15
|
-
* including a '(' or ')' character only if it is backslash-escaped.
|
|
16
|
-
*/
|
|
17
|
-
export function eatLinkTitle(
|
|
18
|
-
nodePoints: ReadonlyArray<INodePoint>,
|
|
19
|
-
startIndex: number,
|
|
20
|
-
endIndex: number,
|
|
21
|
-
): number {
|
|
22
|
-
if (startIndex >= endIndex) return -1
|
|
23
|
-
|
|
24
|
-
let i = startIndex
|
|
25
|
-
const titleWrapSymbol = nodePoints[i].codePoint
|
|
26
|
-
switch (titleWrapSymbol) {
|
|
27
|
-
case AsciiCodePoint.DOUBLE_QUOTE:
|
|
28
|
-
case AsciiCodePoint.SINGLE_QUOTE: {
|
|
29
|
-
for (i += 1; i < endIndex; ++i) {
|
|
30
|
-
const p = nodePoints[i]
|
|
31
|
-
switch (p.codePoint) {
|
|
32
|
-
case AsciiCodePoint.BACKSLASH:
|
|
33
|
-
i += 1
|
|
34
|
-
break
|
|
35
|
-
case titleWrapSymbol:
|
|
36
|
-
return i + 1
|
|
37
|
-
/**
|
|
38
|
-
* Although link titles may span multiple lines, they may not contain a blank line.
|
|
39
|
-
*/
|
|
40
|
-
case VirtualCodePoint.LINE_END: {
|
|
41
|
-
const j = eatOptionalBlankLines(nodePoints, startIndex, i)
|
|
42
|
-
if (nodePoints[j].line > p.line + 1) return -1
|
|
43
|
-
break
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
break
|
|
48
|
-
}
|
|
49
|
-
case AsciiCodePoint.OPEN_PARENTHESIS: {
|
|
50
|
-
let openParens = 1
|
|
51
|
-
for (i += 1; i < endIndex; ++i) {
|
|
52
|
-
const p = nodePoints[i]
|
|
53
|
-
switch (p.codePoint) {
|
|
54
|
-
case AsciiCodePoint.BACKSLASH:
|
|
55
|
-
i += 1
|
|
56
|
-
break
|
|
57
|
-
/**
|
|
58
|
-
* Although link titles may span multiple lines, they may not contain a blank line.
|
|
59
|
-
*/
|
|
60
|
-
case VirtualCodePoint.LINE_END: {
|
|
61
|
-
const j = eatOptionalBlankLines(nodePoints, startIndex, i)
|
|
62
|
-
if (nodePoints[j].line > p.line + 1) return -1
|
|
63
|
-
break
|
|
64
|
-
}
|
|
65
|
-
case AsciiCodePoint.OPEN_PARENTHESIS:
|
|
66
|
-
openParens += 1
|
|
67
|
-
break
|
|
68
|
-
case AsciiCodePoint.CLOSE_PARENTHESIS:
|
|
69
|
-
openParens -= 1
|
|
70
|
-
if (openParens === 0) return i + 1
|
|
71
|
-
break
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
break
|
|
75
|
-
}
|
|
76
|
-
case AsciiCodePoint.CLOSE_PARENTHESIS:
|
|
77
|
-
return i
|
|
78
|
-
default:
|
|
79
|
-
return -1
|
|
80
|
-
}
|
|
81
|
-
return -1
|
|
82
|
-
}
|