Skip to content

Commit fd38c45

Browse files
authored
Change footnote order
Previously, the footer (if footnote definitions were found) showed all footnote definitions, in definition order. Even footnote definitions with the same identifier were transformed to multiple definitions. remarkjs/remark-rehype#11 shows that definition order does not make sense if inline footnotes (`[^Some text]`) are used in combination with footnote references (`[^id]`) and footnote definitions (`[^id]: Some text`). Investigating this, I came to the conclusion that it also does not make sense to output all definitions, whether they are used (or even duplicate) or not. Instead, it makes sense to track the order in which footnotes are used (first), and only output a definition for the used inline footnotes / footnote references, in the order they were used. This is in line with how link definitions are used as well. Closes remarkjs/remark-rehype#11. Closes GH-32.
1 parent 751b54d commit fd38c45

File tree

7 files changed

+195
-31
lines changed

7 files changed

+195
-31
lines changed

lib/footer.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,23 @@ var list = require('./handlers/list')
77
var wrap = require('./wrap')
88

99
function generateFootnotes(h) {
10-
var footnotes = h.footnotes
11-
var length = footnotes.length
10+
var footnoteById = h.footnoteById
11+
var footnoteOrder = h.footnoteOrder
12+
var length = footnoteOrder.length
1213
var index = -1
1314
var listItems = []
1415
var def
1516
var backReference
1617
var content
1718
var tail
1819

19-
if (!length) {
20-
return null
21-
}
22-
2320
while (++index < length) {
24-
def = footnotes[index]
21+
def = footnoteById[footnoteOrder[index].toUpperCase()]
22+
23+
if (!def) {
24+
continue
25+
}
26+
2527
content = def.children.concat()
2628
tail = content[content.length - 1]
2729
backReference = {
@@ -38,12 +40,16 @@ function generateFootnotes(h) {
3840

3941
tail.children.push(backReference)
4042

41-
listItems[index] = {
43+
listItems.push({
4244
type: 'listItem',
4345
data: {hProperties: {id: 'fn-' + def.identifier}},
4446
children: content,
4547
position: def.position
46-
}
48+
})
49+
}
50+
51+
if (listItems.length === 0) {
52+
return null
4753
}
4854

4955
return h(

lib/handlers/footnote-reference.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ module.exports = footnoteReference
55
var u = require('unist-builder')
66

77
function footnoteReference(h, node) {
8+
var footnoteOrder = h.footnoteOrder
89
var identifier = node.identifier
910

11+
if (footnoteOrder.indexOf(identifier) === -1) {
12+
footnoteOrder.push(identifier)
13+
}
14+
1015
return h(node.position, 'sup', {id: 'fnref-' + identifier}, [
1116
h(node, 'a', {href: '#fn-' + identifier, className: ['footnote-ref']}, [
1217
u('text', node.label || identifier)

lib/handlers/footnote.js

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,25 @@ module.exports = footnote
55
var footnoteReference = require('./footnote-reference')
66

77
function footnote(h, node) {
8-
var identifiers = []
8+
var footnoteById = h.footnoteById
9+
var footnoteOrder = h.footnoteOrder
910
var identifier = 1
10-
var footnotes = h.footnotes
11-
var length = footnotes.length
12-
var index = -1
1311

14-
while (++index < length) {
15-
identifiers[index] = footnotes[index].identifier
16-
}
17-
18-
while (identifiers.indexOf(String(identifier)) !== -1) {
12+
while (identifier in footnoteById) {
1913
identifier++
2014
}
2115

2216
identifier = String(identifier)
2317

24-
footnotes.push({
18+
// No need to check if `identifier` exists, as it’s guaranteed to not exist.
19+
footnoteOrder.push(identifier)
20+
21+
footnoteById[identifier] = {
2522
type: 'footnoteDefinition',
2623
identifier: identifier,
2724
children: [{type: 'paragraph', children: node.children}],
2825
position: node.position
29-
})
26+
}
3027

3128
return footnoteReference(h, {
3229
type: 'footnoteReference',

lib/index.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,22 @@ var one = require('./one')
1212
var footer = require('./footer')
1313
var handlers = require('./handlers')
1414

15+
var own = {}.hasOwnProperty
16+
1517
// Factory to transform.
1618
function factory(tree, options) {
1719
var settings = options || {}
1820
var dangerous = settings.allowDangerousHTML
21+
var footnoteById = {}
1922

2023
h.dangerous = dangerous
2124
h.definition = definitions(tree, settings)
22-
h.footnotes = []
25+
h.footnoteById = footnoteById
26+
h.footnoteOrder = []
2327
h.augment = augment
2428
h.handlers = xtend(handlers, settings.handlers || {})
2529

26-
visit(tree, 'footnoteDefinition', visitor)
30+
visit(tree, 'footnoteDefinition', onfootnotedefinition)
2731

2832
return h
2933

@@ -80,19 +84,25 @@ function factory(tree, options) {
8084
})
8185
}
8286

83-
function visitor(definition) {
84-
h.footnotes.push(definition)
87+
function onfootnotedefinition(definition) {
88+
var id = definition.identifier.toUpperCase()
89+
90+
// Mimick CM behavior of link definitions.
91+
// https://github.com/syntax-tree/mdast-util-definitions/blob/8d48e57/index.js#L26
92+
if (!own.call(footnoteById, id)) {
93+
footnoteById[id] = definition
94+
}
8595
}
8696
}
8797

8898
// Transform `tree`, which is an mdast node, to a hast node.
8999
function toHast(tree, options) {
90100
var h = factory(tree, options)
91101
var node = one(h, tree)
92-
var footnotes = footer(h)
102+
var foot = footer(h)
93103

94-
if (node && node.children && footnotes) {
95-
node.children = node.children.concat(u('text', '\n'), footnotes)
104+
if (foot) {
105+
node.children = node.children.concat(u('text', '\n'), foot)
96106
}
97107

98108
return node

test/footnote-definition.js

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,69 @@ var u = require('unist-builder')
55
var to = require('..')
66

77
test('FootnoteDefinition', function(t) {
8-
t.equal(
8+
t.deepEqual(
99
to(
10-
u('footnoteDefinition', {identifier: 'zulu'}, [
11-
u('paragraph', [u('text', 'alpha')])
10+
u('root', [
11+
u('footnoteDefinition', {identifier: 'zulu'}, [
12+
u('paragraph', [u('text', 'alpha')])
13+
])
1214
])
1315
),
14-
null,
16+
u('root', []),
1517
'should ignore `footnoteDefinition`'
1618
)
1719

20+
t.deepEqual(
21+
to(
22+
u('root', [
23+
u('footnoteReference', {identifier: 'zulu'}),
24+
u('footnoteDefinition', {identifier: 'zulu'}, [
25+
u('paragraph', [u('text', 'alpha')])
26+
]),
27+
u('footnoteDefinition', {identifier: 'zulu'}, [
28+
u('paragraph', [u('text', 'bravo')])
29+
])
30+
])
31+
),
32+
u('root', [
33+
u('element', {tagName: 'sup', properties: {id: 'fnref-zulu'}}, [
34+
u(
35+
'element',
36+
{
37+
tagName: 'a',
38+
properties: {href: '#fn-zulu', className: ['footnote-ref']}
39+
},
40+
[u('text', 'zulu')]
41+
)
42+
]),
43+
u('text', '\n'),
44+
u('element', {tagName: 'div', properties: {className: ['footnotes']}}, [
45+
u('text', '\n'),
46+
u('element', {tagName: 'hr', properties: {}}, []),
47+
u('text', '\n'),
48+
u('element', {tagName: 'ol', properties: {}}, [
49+
u('text', '\n'),
50+
u('element', {tagName: 'li', properties: {id: 'fn-zulu'}}, [
51+
u('text', 'alpha'),
52+
u(
53+
'element',
54+
{
55+
tagName: 'a',
56+
properties: {
57+
href: '#fnref-zulu',
58+
className: ['footnote-backref']
59+
}
60+
},
61+
[u('text', '↩')]
62+
)
63+
]),
64+
u('text', '\n')
65+
]),
66+
u('text', '\n')
67+
])
68+
]),
69+
'should use the first `footnoteDefinition` if multiple exist'
70+
)
71+
1872
t.end()
1973
})

test/footnote-mixed.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict'
2+
3+
var test = require('tape')
4+
var u = require('unist-builder')
5+
var to = require('..')
6+
7+
test('Footnote', function(t) {
8+
var mdast = to(
9+
u('root', [
10+
u('paragraph', [
11+
u('text', 'Alpha'),
12+
u('footnote', [u('text', 'Charlie')])
13+
]),
14+
u('paragraph', [
15+
u('text', 'Bravo'),
16+
u('footnoteReference', {identifier: 'x'})
17+
]),
18+
u('footnoteDefinition', {identifier: 'x'}, [
19+
u('paragraph', [u('text', 'Delta')])
20+
])
21+
])
22+
)
23+
24+
var hast = u('root', [
25+
u('element', {tagName: 'p', properties: {}}, [
26+
u('text', 'Alpha'),
27+
u('element', {tagName: 'sup', properties: {id: 'fnref-1'}}, [
28+
u(
29+
'element',
30+
{
31+
tagName: 'a',
32+
properties: {href: '#fn-1', className: ['footnote-ref']}
33+
},
34+
[u('text', '1')]
35+
)
36+
])
37+
]),
38+
u('text', '\n'),
39+
u('element', {tagName: 'p', properties: {}}, [
40+
u('text', 'Bravo'),
41+
u('element', {tagName: 'sup', properties: {id: 'fnref-x'}}, [
42+
u(
43+
'element',
44+
{
45+
tagName: 'a',
46+
properties: {href: '#fn-x', className: ['footnote-ref']}
47+
},
48+
[u('text', 'x')]
49+
)
50+
])
51+
]),
52+
u('text', '\n'),
53+
u('element', {tagName: 'div', properties: {className: ['footnotes']}}, [
54+
u('text', '\n'),
55+
u('element', {tagName: 'hr', properties: {}}, []),
56+
u('text', '\n'),
57+
u('element', {tagName: 'ol', properties: {}}, [
58+
u('text', '\n'),
59+
u('element', {tagName: 'li', properties: {id: 'fn-1'}}, [
60+
u('text', 'Charlie'),
61+
u(
62+
'element',
63+
{
64+
tagName: 'a',
65+
properties: {href: '#fnref-1', className: ['footnote-backref']}
66+
},
67+
[u('text', '↩')]
68+
)
69+
]),
70+
u('text', '\n'),
71+
u('element', {tagName: 'li', properties: {id: 'fn-x'}}, [
72+
u('text', 'Delta'),
73+
u(
74+
'element',
75+
{
76+
tagName: 'a',
77+
properties: {href: '#fnref-x', className: ['footnote-backref']}
78+
},
79+
[u('text', '↩')]
80+
)
81+
]),
82+
u('text', '\n')
83+
]),
84+
u('text', '\n')
85+
])
86+
])
87+
88+
t.deepEqual(mdast, hast, 'should order the footnote section by usage')
89+
90+
t.end()
91+
})

test/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require('./delete.js')
1111
require('./emphasis.js')
1212
require('./footnote-definition.js')
1313
require('./footnote-reference.js')
14+
require('./footnote-mixed.js')
1415
require('./footnote.js')
1516
require('./heading.js')
1617
require('./html.js')

0 commit comments

Comments
 (0)