remark.js is great tool that transforms markdown with plugins.
I usually combine remark-parse, remark-rehype and rehype-react to transform markdown into react components. The configuration of the processor is like:
import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeReact from 'rehype-react'
const processor = unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeReact, {
components: {
//custom components here
},
})
The problem
I recently found that the position of the markdown elements is not available in the react scope. For example, if we have a markdown file like:
# Headline
The expected position prop should be passed to the corresponding react component like:
function Headline(props: { position: { line: { start: number; end: number } } }) {}
This is an important feature for the markdown editor. When the user scrolls the preview view, the editor view should scroll to the corresponding position. If the position of the origin markdown element is available, then the editor knows very well which line to scroll to.
The investigation
This first thing I asked myself to do is checking whether remark already knows the position. Luck me, the answer is yes.
I learnt there by inspect the mdast
(markdown-abstract-syntax-tree). Run the following test.js
and it will generate the description of the tree in json format.
import { unified } from 'unified'
import remarkParse from 'remark-parse'
const processor = unified().use(remarkParse)
const markdown = `# Headline`
console.log(JSON.stringify(processor.parse(markdown), null, 2))
And you will get the result as:
{
"type": "root",
"children": [
{
"type": "heading",
"depth": 1,
"children": [
{
"type": "text",
"value": "Headline",
"position": {
"start": {
"line": 1,
"column": 3,
"offset": 2
},
"end": {
"line": 1,
"column": 11,
"offset": 10
}
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 11,
"offset": 10
}
}
}
],
"position": {
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 11,
"offset": 10
}
}
}
As you can see, the position of the headline is
{
"start": {
"line": 1,
"column": 1,
"offset": 0
},
"end": {
"line": 1,
"column": 11,
"offset": 10
}
}
So which plugin
remove
the position object?
The plugin remark-rehype
is the one that remove the position object. It generates hast
(html-abstract-syntax-tree) from each mdast
using specify handler
. The default handlers don't expose position property to the hast
. Here is the source code of the headline handler.
export function heading(state, node) {
/** @type {Element} */
const result = {
type: 'element',
tagName: 'h' + node.depth,
properties: {},
children: state.all(node),
}
state.patch(node, result)
return state.applyData(node, result)
}
PS: source code
The solution
The answer to the solution is custom all the handlers
of remark-rehype
. The following code is the custom handler for the headline.
/**
* @typedef {import('hast').Element} Element
* @typedef {import('mdast').Heading} Heading
* @typedef {import('../state.js').State} State
*/
import gatherPosition from './gather-position.js'
/**
* Turn an mdast `heading` node into hast.
*
* @param {State} state
* Info passed around.
* @param {Heading} node
* mdast node.
* @returns {Element}
* hast node.
*/
function gatherPosition(node) {
return {
[`data-startline`]: node.position.start.line,
[`data-startcolumn`]: node.position.start.column,
[`data-startoffset`]: node.position.start.offset,
[`data-endline`]: node.position.end.line,
[`data-endcolumn`]: node.position.end.column,
[`data-endoffset`]: node.position.end.offset,
}
}
export function heading(state, node) {
/** @type {Element} */
const result = {
type: 'element',
tagName: 'h' + node.depth,
properties: { ...gatherPosition(node) },
children: state.all(node),
}
state.patch(node, result)
return state.applyData(node, result)
}
The custom handler is almost the same as the default one. The only difference is that it adds the position object to the properties
of the hast
node.
Thanks for reading.
Top comments (0)