Skip to content

Commit 71466c1

Browse files
rowanc1fwkoch
andauthored
✨ NEW: Initial handling of numbering and references (#14)
This commit introduces the initial handling of internal state, to match references to target components. The `eq`, `ref` and `numref` roles are introduced, which can reference `equation` and `figure` directives Co-authored-by: Franklin Koch <franklinwkoch@gmail.com>
1 parent a96f2e2 commit 71466c1

19 files changed

+555
-76
lines changed

README.md

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,27 @@ In the browser:
2424
```html
2525
<!DOCTYPE html>
2626
<html>
27-
<head>
28-
<title>Example Page</title>
29-
<script src="https://cdn.jsdelivr.net/npm/markdown-it@12/dist/markdown-it.min.js"></script>
30-
<script src="https://unpkg.com/markdown-it-docutils"></script>
31-
<link rel="stylesheet" type="text/css" media="screen" href="https://unpkg.com/markdown-it-docutils/dist/css/style.min.css" />
32-
</head>
33-
<body>
34-
<div id="demo"></div>
35-
<script>
36-
const text = window
37-
.markdownit()
38-
.use(window.markdownitDocutils.default)
39-
.render("*a*");
40-
document.getElementById("demo").innerHTML = text
41-
</script>
42-
</body>
27+
<head>
28+
<title>Example Page</title>
29+
<script src="https://cdn.jsdelivr.net/npm/markdown-it@12/dist/markdown-it.min.js"></script>
30+
<script src="https://unpkg.com/markdown-it-docutils"></script>
31+
<link
32+
rel="stylesheet"
33+
type="text/css"
34+
media="screen"
35+
href="https://unpkg.com/markdown-it-docutils/dist/css/style.min.css"
36+
/>
37+
</head>
38+
<body>
39+
<div id="demo"></div>
40+
<script>
41+
const text = window
42+
.markdownit()
43+
.use(window.markdownitDocutils.default)
44+
.render("*a*")
45+
document.getElementById("demo").innerHTML = text
46+
</script>
47+
</body>
4348
</html>
4449
```
4550

@@ -54,7 +59,16 @@ By default (see `parseRoles` option), roles are parsed according to the MyST syn
5459

5560
All roles have a fallback renderer, but the the following are specifically handled:
5661

57-
- `raw`
62+
- HTML:
63+
- `sub`: Subscript (alternatively `subscript`)
64+
- `sup`: Superscript (alternatively `superscript`)
65+
- `abbr`: Abbreviation (alternatively `abbreviation`)
66+
- Referencing
67+
- `eq`: Reference labeled equations
68+
- `ref`: Reference any labeled or named block, showing title
69+
- `numref`: Numbered reference for any labeled or named block (use `Figure %s <my_label>`)
70+
- Basic:
71+
- `raw`
5872

5973
## Supported directives (block extensions)
6074

@@ -99,12 +113,8 @@ All directives have a fallback renderer, but the the following are specifically
99113
- `code-cell`
100114
- Tables:
101115
- `list-table`
102-
- HTML:
103-
- `sub`: Subscript
104-
- `sup`: Superscript
105-
- `abbr`: Abbreviation
106116
- Other:
107-
- `math`
117+
- `math`
108118

109119
## CSS Styling
110120

@@ -153,7 +163,7 @@ Now you can start to adapt the code in `src/index.ts` for your plugin, starting
153163

154164
Modify the test in `tests/fixtures.spec.ts`, to load your plugin, then the "fixtures" in `tests/fixtures`, to provide a set of potential Markdown inputs and expected HTML outputs.
155165

156-
On commits/PRs to the `master` branch, the GH actions will trigger, running the linting, unit tests, and build tests.
166+
On commits/PRs to the `main` branch, the GH actions will trigger, running the linting, unit tests, and build tests.
157167
Additionally setup and uncomment the [codecov](https://about.codecov.io/) action in `.github/workflows/ci.yml`, to provide automated CI coverage.
158168

159169
Finally, you can update the version of your package, e.g.: `npm version patch -m "🚀 RELEASE: v%s"`, push to GitHub; `git push --follow-tags`, build; `npm run build`, and publish; `npm publish`.
@@ -165,12 +175,11 @@ This can be deployed by [GitHub Pages].
165175
[ci-link]: https://github.com/executablebooks/markdown-it-docutils/actions
166176
[npm-badge]: https://img.shields.io/npm/v/markdown-it-docutils.svg
167177
[npm-link]: https://www.npmjs.com/package/markdown-it-docutils
168-
169-
[GitHub Actions]: https://docs.github.com/en/actions
170-
[GitHub Pages]: https://docs.github.com/en/pages
178+
[github actions]: https://docs.github.com/en/actions
179+
[github pages]: https://docs.github.com/en/pages
171180
[prettier]: https://prettier.io/
172181
[eslint]: https://eslint.org/
173-
[Jest]: https://facebook.github.io/jest/
174-
[Rollup]: https://rollupjs.org
182+
[jest]: https://facebook.github.io/jest/
183+
[rollup]: https://rollupjs.org
175184
[npm]: https://www.npmjs.com
176185
[unpkg]: https://unpkg.com/

docs/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,15 @@ <h1>markdown-it-docutils</h1>
7878

7979
```{figure} https://via.placeholder.com/150
8080
:align: center
81+
:name: placeholder
8182

8283
A **caption**
8384
```
8485

86+
The placeholder figure is {numref}`Figure %s <placeholder>`.
87+
The `ref` link is: {ref}`placeholder`.
88+
We can also see {eq}`math_label` which is below!
89+
8590
Tables:
8691

8792
```{list-table} Caption *text*

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"lint": "eslint -c .eslintrc.yml --max-warnings 1 src/**/*.ts tests/**/*.ts",
3333
"lint:fix": "eslint -c .eslintrc.yml --fix src/**/*.ts tests/**/*.ts",
3434
"test": "jest",
35+
"test:watch": "jest --watchAll",
3536
"test:cov": "jest --coverage",
3637
"sass": "sass --style=compressed --source-map --embed-sources src/style/index.sass dist/css/style.min.css && postcss dist/css/style.min.css --use autoprefixer -d dist/css/",
3738
"build:bundles": "rollup -c",

src/directives/images.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** Directives for image visualisation */
22
import type Token from "markdown-it/lib/token"
3+
import { newTarget, Target, TargetKind } from "../state/utils"
34
import { Directive, IDirectiveData } from "./main"
45
import {
56
class_option,
@@ -42,7 +43,7 @@ export class Image extends Directive {
4243
// get URI
4344
const src = uri(data.args[0] || "")
4445

45-
const token = this.createToken("image", "img", 0, { map: data.map })
46+
const token = this.createToken("image", "img", 0, { map: data.map, block: true })
4647
token.attrSet("src", src)
4748
token.attrSet("alt", data.options.alt || "")
4849
// TODO markdown-it default renderer requires the alt as children tokens
@@ -90,7 +91,10 @@ export class Figure extends Image {
9091
}
9192
public has_content = true
9293
run(data: IDirectiveData<keyof Figure["option_spec"]>): Token[] {
93-
const openToken = this.createToken("figure_open", "figure", 1, { map: data.map })
94+
const openToken = this.createToken("figure_open", "figure", 1, {
95+
map: data.map,
96+
block: true
97+
})
9498
if (data.options.figclass) {
9599
openToken.attrJoin("class", data.options.figclass.join(" "))
96100
}
@@ -101,18 +105,38 @@ export class Figure extends Image {
101105
// TODO handle figwidth == "image"?
102106
openToken.attrSet("width", data.options.figwidth)
103107
}
108+
let target: Target | undefined
109+
if (data.options.name) {
110+
// TODO: figure out how to pass silent here
111+
target = newTarget(
112+
this.state,
113+
openToken,
114+
TargetKind.figure,
115+
data.options.name,
116+
// TODO: a better title?
117+
data.body.trim()
118+
)
119+
openToken.attrJoin("class", "numbered")
120+
}
104121
const imageToken = this.create_image(data)
105122
imageToken.map = [data.map[0], data.map[0]]
106123
let captionTokens: Token[] = []
107124
if (data.body) {
108-
const openCaption = this.createToken("figure_caption_open", "figcaption", 1)
125+
const openCaption = this.createToken("figure_caption_open", "figcaption", 1, {
126+
block: true
127+
})
128+
if (target) {
129+
openCaption.attrSet("number", `${target.number}`)
130+
}
109131
// TODO in docutils caption can only be single paragraph (or ignored if comment)
110132
// then additional content is figure legend
111133
const captionBody = this.nestedParse(data.body, data.bodyMap[0])
112-
const closeCaption = this.createToken("figure_caption_close", "figcaption", -1)
134+
const closeCaption = this.createToken("figure_caption_close", "figcaption", -1, {
135+
block: true
136+
})
113137
captionTokens = [openCaption, ...captionBody, closeCaption]
114138
}
115-
const closeToken = this.createToken("figure_close", "figure", -1)
139+
const closeToken = this.createToken("figure_close", "figure", -1, { block: true })
116140
return [openToken, imageToken, ...captionTokens, closeToken]
117141
}
118142
}

src/directives/math.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** Admonitions to visualise programming codes */
22
import type Token from "markdown-it/lib/token"
3+
import { newTarget, Target, TargetKind } from "../state/utils"
34
import { Directive, IDirectiveData } from "./main"
45
import { unchanged } from "./options"
56

@@ -23,8 +24,17 @@ export class Math extends Directive {
2324
})
2425
token.attrSet("class", "math block")
2526
if (data.options.label) {
27+
token.attrSet("id", data.options.label)
28+
const target: Target = newTarget(
29+
this.state,
30+
token,
31+
TargetKind.equation,
32+
data.options.label,
33+
""
34+
)
35+
token.attrSet("number", `${target.number}`)
2636
token.info = data.options.label
27-
token.meta = { label: data.options.label, numbered: true }
37+
token.meta = { label: data.options.label, numbered: true, number: target.number }
2838
}
2939
return [token]
3040
}

src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
directivePlugin,
77
IDirectiveOptions
88
} from "./directives"
9+
import statePlugin from "./state/plugin"
910

1011
export { rolesDefault, rolePlugin, Role }
1112
export { directivesDefault, directivePlugin, Directive }
@@ -31,8 +32,9 @@ const OptionDefaults: IOptions = {
3132
export function docutilsPlugin(md: MarkdownIt, options?: IOptions): void {
3233
const fullOptions = { ...OptionDefaults, ...options }
3334

34-
rolePlugin(md, fullOptions)
35-
directivePlugin(md, fullOptions)
35+
md.use(rolePlugin, fullOptions)
36+
md.use(directivePlugin, fullOptions)
37+
md.use(statePlugin, fullOptions)
3638
}
3739

3840
// Note: Exporting default and the function as a named export.

src/roles/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ export { Role, main } from "./main"
22
export { default as rolePlugin } from "./plugin"
33
export type { IOptions as IRoleOptions } from "./types"
44
export { math } from "./math"
5+
export { html } from "./html"
6+
export { references } from "./references"
57

68
import { main } from "./main"
79
import { math } from "./math"
810
import { html } from "./html"
11+
import { references } from "./references"
912

10-
export const rolesDefault = { ...main, ...html, ...math }
13+
export const rolesDefault = { ...main, ...html, ...math, ...references }

src/roles/plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function runRoles(roles: {
7272
const childTokens = []
7373
for (const child of token.children) {
7474
// TODO role name translations
75-
if (child.type === "role" && child.meta && child.meta.name in roles) {
75+
if (child.type === "role" && child.meta?.name in roles) {
7676
try {
7777
const role = new roles[child.meta.name](state)
7878
const newTokens = role.run({

src/roles/references.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type Token from "markdown-it/lib/token"
2+
import { resolveRefLater, TargetKind } from "../state/utils"
3+
import { IRoleData, Role } from "./main"
4+
5+
const REF_PATTERN = /^(.+?)<([^<>]+)>$/ // e.g. 'Labeled Reference <ref>'
6+
7+
export class Eq extends Role {
8+
run(data: IRoleData): Token[] {
9+
const open = new this.state.Token("ref_open", "a", 1)
10+
const content = new this.state.Token("text", "", 0)
11+
const close = new this.state.Token("ref_close", "a", -1)
12+
resolveRefLater(this.state, { open, content, close }, data.content, {
13+
kind: TargetKind.equation,
14+
contentFromTarget: target => {
15+
return `(${target.number})`
16+
}
17+
})
18+
return [open, content, close]
19+
}
20+
}
21+
22+
export class NumRef extends Role {
23+
run(data: IRoleData): Token[] {
24+
const match = REF_PATTERN.exec(data.content)
25+
const [, modified, ref] = match ?? []
26+
const open = new this.state.Token("ref_open", "a", 1)
27+
const content = new this.state.Token("text", "", 0)
28+
const close = new this.state.Token("ref_close", "a", -1)
29+
resolveRefLater(this.state, { open, content, close }, ref || data.content, {
30+
contentFromTarget: target => {
31+
if (!match) return target.title.trim()
32+
return modified
33+
.replace(/%s/g, String(target.number))
34+
.replace(/\{number\}/g, String(target.number))
35+
.trim()
36+
}
37+
})
38+
return [open, content, close]
39+
}
40+
}
41+
42+
export class Ref extends Role {
43+
run(data: IRoleData): Token[] {
44+
const open = new this.state.Token("ref_open", "a", 1)
45+
const content = new this.state.Token("text", "", 0)
46+
const close = new this.state.Token("ref_close", "a", -1)
47+
resolveRefLater(this.state, { open, content, close }, data.content, {
48+
contentFromTarget: target => {
49+
return target.title
50+
}
51+
})
52+
return [open, content, close]
53+
}
54+
}
55+
56+
export const references = {
57+
eq: Eq,
58+
ref: Ref,
59+
numref: NumRef
60+
}

src/state/plugin.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type MarkdownIt from "markdown-it"
2+
import { RuleCore } from "markdown-it/lib/parser_core"
3+
import type StateCore from "markdown-it/lib/rules_core/state_core"
4+
import { getDocState, Target } from "./utils"
5+
6+
/** Allowed options for state plugin */
7+
export type IOptions = Record<string, never> // TODO: Figure out state numbering options
8+
9+
function numberingRule(options: IOptions): RuleCore {
10+
return (state: StateCore) => {
11+
const env = getDocState(state)
12+
13+
env.references.forEach(ref => {
14+
const { name, tokens, contentFromTarget } = ref
15+
16+
const setError = (details: string, error?: Target) => {
17+
tokens.open.attrJoin("class", "error")
18+
tokens.open.tag = tokens.close.tag = "code"
19+
if (contentFromTarget && error) {
20+
tokens.content.content = contentFromTarget(error)
21+
} else {
22+
tokens.content.content = details
23+
}
24+
return true
25+
}
26+
27+
const target = env.targets[name]
28+
if (!target)
29+
return setError(name, {
30+
kind: ref.kind || "",
31+
name,
32+
title: name,
33+
number: `"${name}"`
34+
})
35+
if (ref.kind && target.kind !== ref.kind) {
36+
return setError(`Reference "${name}" does not match kind "${ref.kind}"`)
37+
}
38+
tokens.open.attrSet("href", `#${target.name}`)
39+
if (target.title) tokens.open.attrSet("title", target.title)
40+
if (contentFromTarget) tokens.content.content = contentFromTarget(target).trim()
41+
})
42+
43+
// TODO: Math that wasn't pre-numbered?
44+
return true
45+
}
46+
}
47+
48+
/**
49+
* Create a rule that runs at the end of a markdown-it parser to go through all
50+
* references and add their targets.
51+
*
52+
* This `Rule` is done *last*, as you may reference a figure/equation, when that `Target`
53+
* has not yet been created. The references call `resolveRefLater` when they are being
54+
* created and pass their tokens such that the content of those tokens can be
55+
* dynamically updated.
56+
*
57+
* @param options (none currently)
58+
* @returns The markdown-it Rule
59+
*/
60+
export default function statePlugin(md: MarkdownIt, options: IOptions): void {
61+
md.core.ruler.push("docutils_number", numberingRule(options))
62+
}

0 commit comments

Comments
 (0)