@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.
Files changed (3) hide show
  1. package/index.js +119 -91
  2. package/package.json +6 -1
  3. package/AGENTS.md +0 -33
package/index.js CHANGED
@@ -1,10 +1,6 @@
1
- const render_footnote_anchor_name = (tokens, idx, opt, env) => {
2
- let n = tokens[idx].meta.id + 1
3
- if (!opt.afterBacklinkSuffixArabicNumerals) n = Number(n).toString()
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 endId = endRefs && endRefs[':' + label]
37
- const footId = footRefs && footRefs[':' + label]
23
+ const endRefs = env.endnotes && env.endnotes.refs
24
+ if (!footRefs && !endRefs) return null
25
+ const key = ':' + label
38
26
 
39
- if (preferEndnote && endId !== undefined) {
40
- return { env: env.endnotes, id: endId, isEndnote: true }
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 (footId !== undefined) {
43
- return { env: env.footnotes, id: footId, isEndnote: false }
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 (endId !== undefined) {
46
- return { env: env.endnotes, id: endId, isEndnote: true }
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 notes = getNotesMeta(token, env)
56
- const isEndnote = !!token.meta.isEndnote
57
- const noteDomPrefix = getDomPrefix(isEndnote)
58
- const displayPrefix = getDisplayPrefix(isEndnote, opt)
59
- notes._refCount = notes._refCount || {}
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 (notes.totalCounts && notes.totalCounts[id] > 1) {
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 && tokens[idx].meta.isEndnote
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 && tokens[idx].meta.isEndnote
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 && tokens[idx].meta.isEndnote
95
- const notes = getNotesMeta(tokens[idx], env)
96
- const counts = notes && notes.totalCounts
97
- const noteDomPrefix = getDomPrefix(!!isEndnote)
98
- const displayPrefix = getDisplayPrefix(!!isEndnote, opt)
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 && counts && counts[idNum] > 1) {
99
+ if (opt.beforeSameBacklink && count > 1) {
101
100
  let links = ''
102
- for (let i = 1; i <= counts[idNum]; 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 (counts && counts[idNum] > 1) {
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 id = fn.length++
199
- fn.refs[':' + label] = id
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
- const token = new state.Token('footnote_open', '', 1)
202
- token.meta = { id, label, isEndnote }
203
- token.level = state.level++
204
- state.tokens.push(token)
205
- fn.positions.push(state.tokens.length - 1)
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
- const closeToken = new state.Token('footnote_close', '', -1)
251
- closeToken.level = --state.level
252
- closeToken.meta = { isEndnote }
253
- state.tokens.push(closeToken)
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
- let found = false
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 (!found || pos === start + 2) { return false; }
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
- if (!fn.list) { fn.list = []; }
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, label, isEndnote: resolved.isEndnote }
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, label: id + 1, isEndnote }
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 endnoteBlocks = []
376
+ const endnoteTokens = []
352
377
 
353
- for (let i = 0; i < tokens.length; i++) {
378
+ let write = 0
379
+ let i = 0
380
+ while (i < tokens.length) {
354
381
  const token = tokens[i]
355
- if (token.type !== 'footnote_open' || !token.meta || !token.meta.isEndnote) continue
356
-
357
- let j = i + 1
358
- while (j < tokens.length) {
359
- const t = tokens[j]
360
- if (t.type === 'footnote_close' && t.meta && t.meta.isEndnote) {
361
- j++
362
- break
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
- j++
394
+ continue
365
395
  }
366
- endnoteBlocks.push(tokens.slice(i, j))
367
- tokens.splice(i, j - i)
368
- i--
396
+ tokens[write++] = token
397
+ i++
369
398
  }
370
399
 
371
- if (endnoteBlocks.length === 0) return
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
- endnoteBlocks.forEach(block => {
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.0",
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.