Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/nodes/footnote-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NodeKind } from './node-kind'
import { Node } from './node'

export class FootnoteDefinition implements Node {
readonly children: Node[] = []
readonly label: string
readonly kind = NodeKind.FootnoteDefinition

constructor (label: string, children: Node[]) {
this.label = label
this.children = children
}

toMarkdown () {
return `[^${this.label}]:${this.children.map(x => x.toMarkdown()).join('')}`
}
}
16 changes: 16 additions & 0 deletions src/nodes/footnote-reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NodeKind } from './node-kind'
import { Node } from './node'

export class FootnoteReference implements Node {
readonly children: Node[] = []
readonly label: string
readonly kind = NodeKind.FootnoteReference

constructor (label: string) {
this.label = label
}

toMarkdown () {
return `[^${this.label}]`
}
}
4 changes: 3 additions & 1 deletion src/nodes/node-kind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const enum NodeKind {
CalloutItem = 1 << 23,
Highlight = 1 << 24,
CJK = 1 << 25,
FootnoteReference = 1 << 26,
FootnoteDefinition = 1 << 27,

// combinations
All = -1,
Expand All @@ -36,6 +38,6 @@ export const enum NodeKind {
Link = InlineLink | ReferenceLink,
Image = InlineImage | ReferenceImage,
Style = Strong | Emphasis | Strikethrough | Highlight,
Inline = NaturualConstructs | InlineCode | Link | Style | HTMLTag | InlineImage | ReferenceImage | Math,
Inline = NaturualConstructs | InlineCode | Link | Style | HTMLTag | InlineImage | ReferenceImage | Math | FootnoteReference,
Block = ~Inline,
}
28 changes: 28 additions & 0 deletions src/parser/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { HTMLTag } from '../nodes/html-tag'
import { Punctuation } from '../nodes/punctuation'
import { ReferenceLink } from '../nodes/reference-link'
import { ReferenceDefinition } from '../nodes/reference-definition'
import { FootnoteReference } from '../nodes/footnote-reference'
import { FootnoteDefinition } from '../nodes/footnote-definition'
import { InlineLink } from '../nodes/inline-link'
import { Node } from '../nodes/node'
import { InlineCode, InlineCodeDelimiter } from '../nodes/inline-code'
Expand Down Expand Up @@ -58,6 +60,7 @@ export function parse (str: string, options: NormalizedPadMarkdownOptions): Docu
let listPrefix = ''
let codeLang = ''
let calloutType = ''
let footnoteLabel = ''
const blockquotePrefix: BlockquotePrefix = '>'
let strongDelimiter: StrongDelimiter = '**'
let emphasisDelimiter: EmphasisDelimiter = '*'
Expand Down Expand Up @@ -178,6 +181,21 @@ export function parse (str: string, options: NormalizedPadMarkdownOptions): Docu
resolve(new Blank(c))
i++
}
else if (state === State.FootnoteLabel && c === ']') {
if (c2 === ']:') {
footnoteLabel = popMarkdown()
i += 2
push(State.FootnoteDefinition)
} else {
resolve(new FootnoteReference(popMarkdown()))
i++
}
}
else if (state === State.FootnoteDefinition && c === '\n') {
resolve(new FootnoteDefinition(footnoteLabel, popNodes()))
resolve(new Blank(c))
i++
}
else if (state === State.LinkText && c === ']') {
linkText = popNodes()
if (c2 === ']:') {
Expand Down Expand Up @@ -247,6 +265,10 @@ export function parse (str: string, options: NormalizedPadMarkdownOptions): Docu
i += 2
}
// state === State.Text
else if (c2 === '[^' && allow(NodeKind.FootnoteReference)) {
push(State.FootnoteLabel)
i += 2
}
else if (c === '[' && allow(NodeKind.Link)) {
push(State.LinkText)
i++
Expand Down Expand Up @@ -423,6 +445,9 @@ export function parse (str: string, options: NormalizedPadMarkdownOptions): Docu
case State.LinkText:
resolve(Punctuation.create('['), ...popNodes())
break
case State.FootnoteLabel:
resolve(Punctuation.create('['), Punctuation.create('^'), ...popNodes())
break
case State.ReferingUrl:
resolve(new SquareQuoted(linkText), Punctuation.create('('), ...popNodes())
break
Expand Down Expand Up @@ -460,6 +485,9 @@ export function parse (str: string, options: NormalizedPadMarkdownOptions): Docu
case State.ReferenceLinkUrl:
resolve(new ReferenceDefinition(linkText, popMarkdown()))
break
case State.FootnoteDefinition:
resolve(new FootnoteDefinition(footnoteLabel, popNodes()))
break
case State.OrderedListItem:
resolve(new OrderedListItem(listPrefix, popNodes()))
break
Expand Down
2 changes: 2 additions & 0 deletions src/parser/state-masks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const stateMasks = {
[State.ImageText]: ~NodeKind.Inline | NodeKind.Image,
[State.ImageReferingUrl]: ~NodeKind.Text,
[State.ReferenceLinkUrl]: ~NodeKind.Text,
[State.FootnoteLabel]: ~NodeKind.Text,
[State.FootnoteDefinition]: NodeKind.Block,
[State.HTMLTag]: ~NodeKind.Text,
[State.ReferingID]: ~NodeKind.Text,
[State.BlockCodeLang]: ~NodeKind.Text,
Expand Down
2 changes: 2 additions & 0 deletions src/parser/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export enum State {
ReferingUrl, // [xx](
ReferingID, // [xx][
ReferenceLinkUrl, // [xx]:
FootnoteLabel, // [^
FootnoteDefinition, // [^xx]:
ImageText, // ![
ImageReferingUrl, // ![xx](
ImageReferingID, // ![xx][
Expand Down
102 changes: 102 additions & 0 deletions test/parser/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,108 @@ describe('parse()', () => {
})
})

describe('FootnoteReference/FootnoteDefinition', () => {
it('should parse footnote reference', () => {
const doc = parse('text[^1]', options)
expect(doc.children).toHaveLength(2)

const [text, ref] = doc.children
expect(text).toMatchObject({
kind: NodeKind.AlphabetNumeric,
text: 'text'
})
expect(ref).toMatchObject({
kind: NodeKind.FootnoteReference,
label: '1'
})
expect(ref.toMarkdown()).toEqual('[^1]')
})

it('should parse footnote reference with non-numeric label', () => {
const doc = parse('[^foo-bar]', options)
expect(doc.children).toHaveLength(1)
expect(doc.children[0]).toMatchObject({
kind: NodeKind.FootnoteReference,
label: 'foo-bar'
})
expect(doc.children[0].toMarkdown()).toEqual('[^foo-bar]')
})

it('should parse footnote definition and split its body into inline nodes', () => {
const doc = parse('[^1]: 脚注的具体解释内容123结束', options)
expect(doc.children).toHaveLength(1)

const dfn = doc.children[0]
expect(dfn).toMatchObject({
kind: NodeKind.FootnoteDefinition,
label: '1'
})
expect(dfn.toMarkdown()).toEqual('[^1]: 脚注的具体解释内容123结束')

const numberNode = dfn.children.find(
(n: any) => n.kind === NodeKind.AlphabetNumeric && n.text === '123'
)
expect(numberNode).toBeDefined()
})

it('should parse multiple footnote definitions separated by newline', () => {
const doc = parse('[^1]: foo\n[^2]: bar', options)
expect(doc.children).toHaveLength(3)

const [dfn1, blank, dfn2] = doc.children
expect(dfn1).toMatchObject({
kind: NodeKind.FootnoteDefinition,
label: '1'
})
expect(dfn1.toMarkdown()).toEqual('[^1]: foo')

expect(blank).toMatchObject({
kind: NodeKind.Blank,
char: '\n'
})

expect(dfn2).toMatchObject({
kind: NodeKind.FootnoteDefinition,
label: '2'
})
expect(dfn2.toMarkdown()).toEqual('[^2]: bar')
})

it('should close unterminated footnote definition at EOF (no trailing newline)', () => {
const doc = parse('[^1]: foo', options)
expect(doc.children).toHaveLength(1)
expect(doc.children[0]).toMatchObject({
kind: NodeKind.FootnoteDefinition,
label: '1'
})
expect(doc.children[0].toMarkdown()).toEqual('[^1]: foo')
})

it('should not break existing ReferenceLink/ReferenceDefinition parsing', () => {
const doc1 = parse('[foo][bar]', options)
expect(doc1.children[0]).toMatchObject({
kind: NodeKind.ReferenceLink,
target: 'bar'
})

const doc2 = parse('[foo]: http://harttle.land', options)
expect(doc2.children[0]).toMatchObject({
kind: NodeKind.ReferenceDefinition,
target: ' http://harttle.land'
})
})

// Known limitation: kept consistent with ReferenceLinkUrl's existing lenient
// behavior — a "[^x]:" appearing mid-line (not at the start of a block) is
// still parsed as a footnote definition, swallowing the rest of the line.
it('should (leniently) treat "[^x]:" mid-line as a footnote definition, matching ReferenceLinkUrl behavior', () => {
const doc = parse('text [^1]: not really a definition', options)
const dfn = doc.children.find((n: any) => n.kind === NodeKind.FootnoteDefinition)
expect(dfn).toBeDefined()
expect((dfn as any).label).toEqual('1')
})
})

describe('HTMLTag', () => {
it('should parse anchor tag', () => {
const doc = parse('<http://harttle.land>', options)
Expand Down
21 changes: 21 additions & 0 deletions test/transformers/pad-markdown.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,27 @@ describe('padding()', () => {
})
})

describe('footnotes', () => {
it('should pad inside footnote definition body', () => {
expect(padMarkdown('[^1]: 说明内容123结束'))
.toEqual('[^1]: 说明内容 123 结束')
})
it('should not pad the footnote label itself', () => {
expect(padMarkdown('text[^1]more text'))
.toEqual('text [^1] more text')
})
it('should not pad non-numeric footnote labels', () => {
expect(padMarkdown('[^foo-bar]: 说明内容abc结束'))
.toEqual('[^foo-bar]: 说明内容 abc 结束')
expect(padMarkdown('[^id不应该pad]: 说明内容123结束'))
.toEqual('[^id不应该pad]: 说明内容 123 结束')
})
it('should pad between footnote reference and surrounding CJK text', () => {
expect(padMarkdown('这是正文[^1]继续写'))
.toEqual('这是正文 [^1] 继续写')
})
})

describe('images', () => {
it('should pad between footnote links', () => {
expect(padMarkdown('refer to![foo][bar]please'))
Expand Down
Loading