@peaceroad/markdown-it-footnote-here 0.3.0 → 0.3.2
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/index.js +119 -91
- package/package.json +6 -1
- package/AGENTS.md +0 -33
package/index.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
const render_footnote_anchor_name = (tokens, idx,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
let prefix = ''
|
|
5
|
-
if (typeof env.docId === 'string') {
|
|
6
|
-
prefix = '-' + env.docId + '-'
|
|
7
|
-
}
|
|
1
|
+
const render_footnote_anchor_name = (tokens, idx, _opt, env) => {
|
|
2
|
+
const n = tokens[idx].meta.id + 1
|
|
3
|
+
const prefix = typeof env.docId === 'string' ? `-${env.docId}-` : ''
|
|
8
4
|
return prefix + n
|
|
9
5
|
}
|
|
10
6
|
|
|
@@ -22,28 +18,29 @@ const ensureNotesEnv = (env, key) => {
|
|
|
22
18
|
|
|
23
19
|
const ENDNOTE_DOM_PREFIX = 'en'
|
|
24
20
|
|
|
25
|
-
const getDomPrefix = (isEndnote) => (isEndnote ? ENDNOTE_DOM_PREFIX : 'fn')
|
|
26
|
-
const getDisplayPrefix = (isEndnote, opt) => (isEndnote ? opt.endnotesLabelPrefix : '')
|
|
27
|
-
|
|
28
|
-
const getNotesMeta = (token, env) => {
|
|
29
|
-
if (token.meta && token.meta.isEndnote) return env.endnotes
|
|
30
|
-
return env.footnotes
|
|
31
|
-
}
|
|
32
|
-
|
|
33
21
|
const selectNoteEnv = (label, env, preferEndnote) => {
|
|
34
|
-
const endRefs = env.endnotes && env.endnotes.refs
|
|
35
22
|
const footRefs = env.footnotes && env.footnotes.refs
|
|
36
|
-
const
|
|
37
|
-
|
|
23
|
+
const endRefs = env.endnotes && env.endnotes.refs
|
|
24
|
+
if (!footRefs && !endRefs) return null
|
|
25
|
+
const key = ':' + label
|
|
38
26
|
|
|
39
|
-
if (preferEndnote &&
|
|
40
|
-
|
|
27
|
+
if (preferEndnote && endRefs) {
|
|
28
|
+
const endId = endRefs[key]
|
|
29
|
+
if (endId !== undefined) {
|
|
30
|
+
return { env: env.endnotes, id: endId, isEndnote: true }
|
|
31
|
+
}
|
|
41
32
|
}
|
|
42
|
-
if (
|
|
43
|
-
|
|
33
|
+
if (footRefs) {
|
|
34
|
+
const footId = footRefs[key]
|
|
35
|
+
if (footId !== undefined) {
|
|
36
|
+
return { env: env.footnotes, id: footId, isEndnote: false }
|
|
37
|
+
}
|
|
44
38
|
}
|
|
45
|
-
if (
|
|
46
|
-
|
|
39
|
+
if (!preferEndnote && endRefs) {
|
|
40
|
+
const endId = endRefs[key]
|
|
41
|
+
if (endId !== undefined) {
|
|
42
|
+
return { env: env.endnotes, id: endId, isEndnote: true }
|
|
43
|
+
}
|
|
47
44
|
}
|
|
48
45
|
return null
|
|
49
46
|
}
|
|
@@ -52,18 +49,19 @@ const render_footnote_ref = (tokens, idx, opt, env) => {
|
|
|
52
49
|
const token = tokens[idx]
|
|
53
50
|
const id = token.meta.id
|
|
54
51
|
const n = id + 1
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const noteDomPrefix =
|
|
58
|
-
const displayPrefix =
|
|
59
|
-
notes.
|
|
60
|
-
let refIdx = (notes._refCount[id] = (notes._refCount[id] || 0) + 1)
|
|
61
|
-
if (!opt.afterBacklinkSuffixArabicNumerals) {
|
|
62
|
-
refIdx = String.fromCharCode(96 + refIdx)
|
|
63
|
-
}
|
|
52
|
+
const isEndnote = token.meta.isEndnote
|
|
53
|
+
const notes = isEndnote ? env.endnotes : env.footnotes
|
|
54
|
+
const noteDomPrefix = isEndnote ? ENDNOTE_DOM_PREFIX : 'fn'
|
|
55
|
+
const displayPrefix = isEndnote ? opt.endnotesLabelPrefix : ''
|
|
56
|
+
const totalCounts = notes.totalCounts ? notes.totalCounts[id] || 0 : 0
|
|
64
57
|
let suffix = ''
|
|
65
58
|
let label = `${opt.labelBra}${displayPrefix}${n}${opt.labelKet}`
|
|
66
|
-
if (
|
|
59
|
+
if (totalCounts > 1) {
|
|
60
|
+
const refCount = notes._refCount || (notes._refCount = [])
|
|
61
|
+
let refIdx = (refCount[id] = (refCount[id] || 0) + 1)
|
|
62
|
+
if (!opt.afterBacklinkSuffixArabicNumerals) {
|
|
63
|
+
refIdx = String.fromCharCode(96 + refIdx)
|
|
64
|
+
}
|
|
67
65
|
suffix = '-' + refIdx
|
|
68
66
|
if (opt.beforeSameBacklink) {
|
|
69
67
|
label = `${opt.labelBra}${displayPrefix}${n}${suffix}${opt.labelKet}`
|
|
@@ -77,13 +75,13 @@ const render_footnote_ref = (tokens, idx, opt, env) => {
|
|
|
77
75
|
|
|
78
76
|
const render_footnote_open = (tokens, idx, opt, env, slf) => {
|
|
79
77
|
const id = slf.rules.footnote_anchor_name(tokens, idx, opt, env, slf)
|
|
80
|
-
const isEndnote = tokens[idx].meta
|
|
78
|
+
const isEndnote = tokens[idx].meta.isEndnote
|
|
81
79
|
if (isEndnote) return `<li id="${ENDNOTE_DOM_PREFIX}${id}">\n`
|
|
82
80
|
return `<aside id="fn${id}" class="fn" role="doc-footnote">\n`
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
const render_footnote_close = (tokens, idx) => {
|
|
86
|
-
const isEndnote = tokens[idx].meta
|
|
84
|
+
const isEndnote = tokens[idx].meta.isEndnote
|
|
87
85
|
if (isEndnote) return `</li>\n`
|
|
88
86
|
return `</aside>\n`
|
|
89
87
|
}
|
|
@@ -91,15 +89,16 @@ const render_footnote_close = (tokens, idx) => {
|
|
|
91
89
|
const render_footnote_anchor = (tokens, idx, opt, env) => {
|
|
92
90
|
const idNum = tokens[idx].meta.id
|
|
93
91
|
const n = idNum + 1
|
|
94
|
-
const isEndnote = tokens[idx].meta
|
|
95
|
-
const notes =
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
92
|
+
const isEndnote = tokens[idx].meta.isEndnote
|
|
93
|
+
const notes = isEndnote ? env.endnotes : env.footnotes
|
|
94
|
+
const totalCounts = notes.totalCounts
|
|
95
|
+
const count = totalCounts ? totalCounts[idNum] || 0 : 0
|
|
96
|
+
const noteDomPrefix = isEndnote ? ENDNOTE_DOM_PREFIX : 'fn'
|
|
97
|
+
const displayPrefix = isEndnote ? opt.endnotesLabelPrefix : ''
|
|
99
98
|
|
|
100
|
-
if (opt.beforeSameBacklink &&
|
|
99
|
+
if (opt.beforeSameBacklink && count > 1) {
|
|
101
100
|
let links = ''
|
|
102
|
-
for (let i = 1; i <=
|
|
101
|
+
for (let i = 1; i <= count; i++) {
|
|
103
102
|
const suffix = '-' + String.fromCharCode(96 + i); // a, b, c ...
|
|
104
103
|
links += `<a href="#${noteDomPrefix}-ref${n}${suffix}" class="${noteDomPrefix}-backlink" role="doc-backlink">${opt.backLabelBra}${displayPrefix}${n}${suffix}${opt.backLabelKet}</a>`
|
|
105
104
|
}
|
|
@@ -110,7 +109,7 @@ const render_footnote_anchor = (tokens, idx, opt, env) => {
|
|
|
110
109
|
return `<span class="${noteDomPrefix}-label">${opt.backLabelBra}${displayPrefix}${n}${opt.backLabelKet}</span> `
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
if (
|
|
112
|
+
if (count > 1) {
|
|
114
113
|
return `<a href="#${noteDomPrefix}-ref${n}-a" class="${noteDomPrefix}-backlink" role="doc-backlink">${opt.backLabelBra}${displayPrefix}${n}${opt.backLabelKet}</a> `
|
|
115
114
|
}
|
|
116
115
|
|
|
@@ -195,14 +194,24 @@ const footnote_plugin = (md, option) =>{
|
|
|
195
194
|
|
|
196
195
|
const isEndnote = isEndnoteLabel(label, opt)
|
|
197
196
|
const fn = ensureNotesEnv(state.env, isEndnote ? 'endnotes' : 'footnotes')
|
|
198
|
-
const
|
|
199
|
-
fn.refs[
|
|
197
|
+
const refKey = ':' + label
|
|
198
|
+
const existingId = fn.refs[refKey]
|
|
199
|
+
const isDuplicate = isEndnote && existingId !== undefined
|
|
200
|
+
const id = isDuplicate ? existingId : fn.length++
|
|
201
|
+
if (!isDuplicate) {
|
|
202
|
+
fn.refs[refKey] = id
|
|
203
|
+
}
|
|
200
204
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
205
|
+
let tokenStart = 0
|
|
206
|
+
if (!isDuplicate) {
|
|
207
|
+
const token = new state.Token('footnote_open', '', 1)
|
|
208
|
+
token.meta = { id, isEndnote }
|
|
209
|
+
token.level = state.level++
|
|
210
|
+
state.tokens.push(token)
|
|
211
|
+
fn.positions.push(state.tokens.length - 1)
|
|
212
|
+
} else {
|
|
213
|
+
tokenStart = state.tokens.length
|
|
214
|
+
}
|
|
206
215
|
|
|
207
216
|
const oldBMark = bMarks[startLine]
|
|
208
217
|
const oldTShift = tShift[startLine]
|
|
@@ -247,10 +256,14 @@ const footnote_plugin = (md, option) =>{
|
|
|
247
256
|
state.sCount[startLine] = oldSCount
|
|
248
257
|
state.bMarks[startLine] = oldBMark
|
|
249
258
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
259
|
+
if (!isDuplicate) {
|
|
260
|
+
const closeToken = new state.Token('footnote_close', '', -1)
|
|
261
|
+
closeToken.level = --state.level
|
|
262
|
+
closeToken.meta = { isEndnote }
|
|
263
|
+
state.tokens.push(closeToken)
|
|
264
|
+
} else {
|
|
265
|
+
state.tokens.length = tokenStart
|
|
266
|
+
}
|
|
254
267
|
|
|
255
268
|
return true
|
|
256
269
|
}
|
|
@@ -266,14 +279,13 @@ const footnote_plugin = (md, option) =>{
|
|
|
266
279
|
}
|
|
267
280
|
|
|
268
281
|
let pos = start + 2
|
|
269
|
-
|
|
270
|
-
for (; pos < posMax && !found; pos++) {
|
|
282
|
+
for (; pos < posMax; pos++) {
|
|
271
283
|
const ch = src.charCodeAt(pos)
|
|
284
|
+
if (ch === 0x5D /* ] */) break
|
|
272
285
|
if (ch === 0x20 || ch === 0x0A) { return false; } // space or linebreak
|
|
273
|
-
if (ch === 0x5D /* ] */) { found = true; break; }
|
|
274
286
|
}
|
|
275
287
|
|
|
276
|
-
if (
|
|
288
|
+
if (pos >= posMax || pos === start + 2) { return false; }
|
|
277
289
|
pos++; // pos set next ']' position.
|
|
278
290
|
|
|
279
291
|
const label = src.slice(start + 2, pos - 1)
|
|
@@ -285,16 +297,11 @@ const footnote_plugin = (md, option) =>{
|
|
|
285
297
|
if (!silent) {
|
|
286
298
|
const fn = resolved.env
|
|
287
299
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const footnoteId = fn.list.length
|
|
291
|
-
fn.list[footnoteId] = { label, count: 0 }
|
|
292
|
-
|
|
293
|
-
fn.totalCounts = fn.totalCounts || {}
|
|
300
|
+
fn.totalCounts = fn.totalCounts || []
|
|
294
301
|
fn.totalCounts[resolved.id] = (fn.totalCounts[resolved.id] || 0) + 1
|
|
295
302
|
|
|
296
303
|
const token = state.push('footnote_ref', '', 0)
|
|
297
|
-
token.meta = { id: resolved.id,
|
|
304
|
+
token.meta = { id: resolved.id, isEndnote: resolved.isEndnote }
|
|
298
305
|
}
|
|
299
306
|
|
|
300
307
|
state.pos = pos
|
|
@@ -306,7 +313,7 @@ const footnote_plugin = (md, option) =>{
|
|
|
306
313
|
const tokens = state.tokens
|
|
307
314
|
const createAnchorToken = (id, isEndnote) => {
|
|
308
315
|
const aToken = new state.Token('footnote_anchor', '', 0)
|
|
309
|
-
aToken.meta = { id,
|
|
316
|
+
aToken.meta = { id, isEndnote }
|
|
310
317
|
return aToken
|
|
311
318
|
}
|
|
312
319
|
|
|
@@ -314,6 +321,30 @@ const footnote_plugin = (md, option) =>{
|
|
|
314
321
|
const positions = notes && notes.positions
|
|
315
322
|
if (!positions || positions.length === 0) { return; }
|
|
316
323
|
|
|
324
|
+
if (opt.afterBacklink) {
|
|
325
|
+
const noteDomPrefix = isEndnote ? ENDNOTE_DOM_PREFIX : 'fn'
|
|
326
|
+
const totalCounts = notes.totalCounts
|
|
327
|
+
for (let j = 0, len = positions.length; j < len; ++j) {
|
|
328
|
+
const posOpen = positions[j]
|
|
329
|
+
if (posOpen + 2 >= tokens.length) continue
|
|
330
|
+
|
|
331
|
+
const t1 = tokens[posOpen + 1]
|
|
332
|
+
if (t1.type !== 'paragraph_open') continue
|
|
333
|
+
|
|
334
|
+
const t2 = tokens[posOpen + 2]
|
|
335
|
+
if (t2.type !== 'inline') continue
|
|
336
|
+
|
|
337
|
+
const t0 = tokens[posOpen]
|
|
338
|
+
const id = t0.meta.id
|
|
339
|
+
|
|
340
|
+
t2.children.unshift(createAnchorToken(id, isEndnote))
|
|
341
|
+
const n = id + 1
|
|
342
|
+
const counts = totalCounts && totalCounts[id]
|
|
343
|
+
t2.children.push(createAfterBackLinkToken(state, counts, n, opt, noteDomPrefix, isEndnote))
|
|
344
|
+
}
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
317
348
|
for (let j = 0, len = positions.length; j < len; ++j) {
|
|
318
349
|
const posOpen = positions[j]
|
|
319
350
|
if (posOpen + 2 >= tokens.length) continue
|
|
@@ -326,14 +357,8 @@ const footnote_plugin = (md, option) =>{
|
|
|
326
357
|
|
|
327
358
|
const t0 = tokens[posOpen]
|
|
328
359
|
const id = t0.meta.id
|
|
329
|
-
const noteDomPrefix = isEndnote ? ENDNOTE_DOM_PREFIX : 'fn'
|
|
330
360
|
|
|
331
361
|
t2.children.unshift(createAnchorToken(id, isEndnote))
|
|
332
|
-
if (opt.afterBacklink) {
|
|
333
|
-
const n = id + 1
|
|
334
|
-
const counts = notes.totalCounts && notes.totalCounts[id]
|
|
335
|
-
t2.children.push(createAfterBackLinkToken(state, counts, n, opt, noteDomPrefix, isEndnote))
|
|
336
|
-
}
|
|
337
362
|
}
|
|
338
363
|
}
|
|
339
364
|
|
|
@@ -348,27 +373,31 @@ const footnote_plugin = (md, option) =>{
|
|
|
348
373
|
}
|
|
349
374
|
|
|
350
375
|
const tokens = state.tokens
|
|
351
|
-
const
|
|
376
|
+
const endnoteTokens = []
|
|
352
377
|
|
|
353
|
-
|
|
378
|
+
let write = 0
|
|
379
|
+
let i = 0
|
|
380
|
+
while (i < tokens.length) {
|
|
354
381
|
const token = tokens[i]
|
|
355
|
-
if (token.type
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
382
|
+
if (token.type === 'footnote_open' && token.meta && token.meta.isEndnote) {
|
|
383
|
+
endnoteTokens.push(token)
|
|
384
|
+
i++
|
|
385
|
+
while (i < tokens.length) {
|
|
386
|
+
const t = tokens[i]
|
|
387
|
+
endnoteTokens.push(t)
|
|
388
|
+
if (t.type === 'footnote_close' && t.meta && t.meta.isEndnote) {
|
|
389
|
+
i++
|
|
390
|
+
break
|
|
391
|
+
}
|
|
392
|
+
i++
|
|
363
393
|
}
|
|
364
|
-
|
|
394
|
+
continue
|
|
365
395
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
i--
|
|
396
|
+
tokens[write++] = token
|
|
397
|
+
i++
|
|
369
398
|
}
|
|
370
399
|
|
|
371
|
-
if (
|
|
400
|
+
if (endnoteTokens.length === 0) return
|
|
372
401
|
|
|
373
402
|
const sectionOpen = new state.Token('html_block', '', 0)
|
|
374
403
|
const attrs = []
|
|
@@ -388,10 +417,9 @@ const footnote_plugin = (md, option) =>{
|
|
|
388
417
|
const sectionClose = new state.Token('html_block', '', 0)
|
|
389
418
|
sectionClose.content = '</ol>\n</section>\n'
|
|
390
419
|
|
|
420
|
+
tokens.length = write
|
|
391
421
|
tokens.push(sectionOpen)
|
|
392
|
-
|
|
393
|
-
tokens.push(...block)
|
|
394
|
-
})
|
|
422
|
+
for (let j = 0; j < endnoteTokens.length; j++) tokens.push(endnoteTokens[j])
|
|
395
423
|
tokens.push(sectionClose)
|
|
396
424
|
}
|
|
397
425
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peaceroad/markdown-it-footnote-here",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A markdown-it plugin. This generate aside[role|doc-footnote] element just below the footnote reference paragraph.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type":"module",
|
|
7
|
+
"files" : [
|
|
8
|
+
"index.js",
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
7
12
|
"author": "peaceroad <peaceroad@gmail.com>",
|
|
8
13
|
"homepage": "https://github.com/peaceroad/markdown-it-footnote-here#readme",
|
|
9
14
|
"repository": {
|
package/AGENTS.md
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Workflow for Updating markdown-it-footnote-here
|
|
2
|
-
|
|
3
|
-
This document captures the current implementation workflow, especially around footnotes/endnotes.
|
|
4
|
-
|
|
5
|
-
## Code overview
|
|
6
|
-
- `index.js`:
|
|
7
|
-
- Rendering helpers: `render_footnote_ref`, `render_footnote_open/close`, `render_footnote_anchor`, and `createAfterBackLinkToken`.
|
|
8
|
-
- Parsing: custom block rule `footnote_def`, inline rule `footnote_ref`, core rule `footnote_anchor`, and `endnotes_move` to append endnotes at the end.
|
|
9
|
-
- Endnote handling: labels starting with `endnotesPrefix` (default `en-`) are endnotes; DOM ids/classes use fixed `en`.
|
|
10
|
-
- Options include backlink placement, label customization, and endnotes section attributes.
|
|
11
|
-
|
|
12
|
-
## Adding features / making changes
|
|
13
|
-
1) Review options and defaults in `index.js` (`opt` object).
|
|
14
|
-
2) Keep DOM prefixes stable:
|
|
15
|
-
- Footnotes use `fn`; endnotes use `en` for ids/classes (`en1`, `en-ref1`, etc.).
|
|
16
|
-
3) Parsing flow:
|
|
17
|
-
- `footnote_def` registers notes into `env.footnotes` or `env.endnotes` based on `endnotesPrefix`.
|
|
18
|
-
- `footnote_ref` resolves references via `selectNoteEnv` and tags tokens with `isEndnote`.
|
|
19
|
-
4) Rendering flow:
|
|
20
|
-
- `footnote_anchor` injects backlinks/labels into footnote content.
|
|
21
|
-
- `endnotes_move` removes endnote blocks from inline positions and appends a `<section>` (attributes ordered aria-label → id → class → role).
|
|
22
|
-
5) When adding options:
|
|
23
|
-
- Update `README.md` and add fixtures under `test/`.
|
|
24
|
-
- Extend `test/test.js` to load the new fixtures.
|
|
25
|
-
|
|
26
|
-
## Testing
|
|
27
|
-
- Run `npm test` (uses `test/test.js` and fixtures under `test/`).
|
|
28
|
-
- Fixtures format: alternating `[Markdown]` and `[HTML]` blocks.
|
|
29
|
-
|
|
30
|
-
## Notes
|
|
31
|
-
- `labelSupTag` applies to both footnotes and endnotes.
|
|
32
|
-
- If `endnotesPrefix` is empty, endnotes are disabled.
|
|
33
|
-
- `endnotesUseHeading` true: render `<h2>` with `endnotesSectionAriaLabel`; false: use `aria-label` without heading.
|