Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 85 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,67 @@ import type {
Parent,
PhrasingContent
} from 'mdast'
import type {Position} from 'unist'

export {directiveFromMarkdown, directiveToMarkdown} from './lib/index.js'

/**
* Configuration.
*/
export interface FromMarkdownOptions {
/**
* Whether to add extra positional info about attributes to directives
* (default: `false`).
*
* When `true`, directives with attributes get
* `data.position.attributes`.
*/
verbose?: boolean | null | undefined
}

/**
* Positional info of an attribute of a directive.
*
* `start` and `end` span the whole attribute:
* `#a`, `.a`, `a`, `a=b`, `a="b"`.
*/
export interface DirectiveAttributePosition extends Position {
/**
* Attribute name (`id` for `#a`, `class` for `.a`).
*/
name: string

/**
* Positional info of the value, without quotes
* (such as `b` in `a="b"`, or `a` in `#a`);
* empty between the quotes for an empty quoted value (`a=""`);
* not present when there is no value (`a`).
*
* Character references are not decoded:
* this is the place of the raw value in the source.
*/
value?: Position | undefined
}

/**
* Extra positional info of a directive.
*/
export interface DirectiveDataPosition {
/**
* Positional info of every attribute, in source order.
*
* Duplicate attributes are all included.
* For a given name, the last one is the one whose value is in
* `attributes`,
* except for `class`,
* where the values of all of them are joined.
*
* Field added by `mdast-util-directive` when passing `verbose: true` to
* `directiveFromMarkdown`.
*/
attributes: Array<DirectiveAttributePosition>
}

/**
* Configuration.
*/
Expand Down Expand Up @@ -77,7 +135,13 @@ export interface ContainerDirective extends DirectiveFields, Parent {
/**
* Info associated with mdast container directive nodes by the ecosystem.
*/
export interface ContainerDirectiveData extends Data {}
export interface ContainerDirectiveData extends Data {
/**
* Extra positional info,
* added when passing `verbose: true` to `directiveFromMarkdown`.
*/
position?: DirectiveDataPosition | undefined
}

/**
* Markdown directive (leaf form).
Expand All @@ -102,7 +166,13 @@ export interface LeafDirective extends Parent, DirectiveFields {
/**
* Info associated with mdast leaf directive nodes by the ecosystem.
*/
export interface LeafDirectiveData extends Data {}
export interface LeafDirectiveData extends Data {
/**
* Extra positional info,
* added when passing `verbose: true` to `directiveFromMarkdown`.
*/
position?: DirectiveDataPosition | undefined
}

/**
* Markdown directive (text form).
Expand All @@ -127,7 +197,13 @@ export interface TextDirective extends DirectiveFields, Parent {
/**
* Info associated with mdast text directive nodes by the ecosystem.
*/
export interface TextDirectiveData extends Data {}
export interface TextDirectiveData extends Data {
/**
* Extra positional info,
* added when passing `verbose: true` to `directiveFromMarkdown`.
*/
position?: DirectiveDataPosition | undefined
}

/**
* Union of registered mdast directive nodes.
Expand All @@ -143,6 +219,12 @@ declare module 'mdast-util-from-markdown' {
* Attributes for current directive.
*/
directiveAttributes?: Array<[string, string]> | undefined

/**
* Positional info of attributes for current directive,
* when `verbose`.
*/
directiveAttributePositions?: Array<DirectiveAttributePosition> | undefined
}
}

Expand Down
118 changes: 115 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* @import {
* Directives,
* FromMarkdownOptions,
* LeafDirective,
* TextDirective,
* ToMarkdownOptions
Expand All @@ -18,6 +19,7 @@
* State
* } from 'mdast-util-to-markdown'
* @import {Nodes, Paragraph} from 'mdast'
* @import {Point} from 'unist'
*/

import {ccount} from 'ccount'
Expand All @@ -29,6 +31,9 @@ import {visitParents} from 'unist-util-visit-parents'

const own = {}.hasOwnProperty

/** @type {Readonly<FromMarkdownOptions>} */
const emptyFromMarkdownOptions = {}

/** @type {Readonly<ToMarkdownOptions>} */
const emptyOptions = {}

Expand All @@ -39,10 +44,17 @@ const unquoted = /^[^\t\n\r "'<=>`}]+$/
* Create an extension for `mdast-util-from-markdown` to enable directives in
* markdown.
*
* @param {Readonly<FromMarkdownOptions> | null | undefined} [options]
* Configuration (optional).
* @returns {FromMarkdownExtension}
* Extension for `mdast-util-from-markdown` to enable directives.
*/
export function directiveFromMarkdown() {
export function directiveFromMarkdown(options) {
const settings = options || emptyFromMarkdownOptions
const enterAttributes = settings.verbose
? enterAttributesVerbose
: enterAttributesTerse

return {
canContainEols: ['textDirective'],
enter: {
Expand All @@ -62,6 +74,7 @@ export function directiveFromMarkdown() {
directiveContainerAttributeIdValue: exitAttributeIdValue,
directiveContainerAttributeName: exitAttributeName,
directiveContainerAttributeValue: exitAttributeValue,
directiveContainerAttributeValueLiteral: exitAttributeValueLiteral,
directiveContainerAttributes: exitAttributes,
directiveContainerLabel: exitContainerLabel,
directiveContainerName: exitName,
Expand All @@ -71,6 +84,7 @@ export function directiveFromMarkdown() {
directiveLeafAttributeIdValue: exitAttributeIdValue,
directiveLeafAttributeName: exitAttributeName,
directiveLeafAttributeValue: exitAttributeValue,
directiveLeafAttributeValueLiteral: exitAttributeValueLiteral,
directiveLeafAttributes: exitAttributes,
directiveLeafName: exitName,

Expand All @@ -79,6 +93,7 @@ export function directiveFromMarkdown() {
directiveTextAttributeIdValue: exitAttributeIdValue,
directiveTextAttributeName: exitAttributeName,
directiveTextAttributeValue: exitAttributeValue,
directiveTextAttributeValueLiteral: exitAttributeValueLiteral,
directiveTextAttributes: exitAttributes,
directiveTextName: exitName
}
Expand Down Expand Up @@ -429,11 +444,22 @@ function exitContainerLabel(token) {
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterAttributes() {
function enterAttributesTerse() {
this.data.directiveAttributes = []
this.buffer() // Capture EOLs
}

/**
* Handle the start of attributes, tracking their positional info.
*
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterAttributesVerbose(token) {
enterAttributesTerse.call(this, token)
this.data.directiveAttributePositions = []
}

/**
* Handle the end of an ID attribute value.
*
Expand All @@ -447,6 +473,7 @@ function exitAttributeIdValue(token) {
'id',
parseEntities(this.sliceSerialize(token), {attribute: true})
])
addShortcutPosition(this, 'id', token)
}

/**
Expand All @@ -462,6 +489,7 @@ function exitAttributeClassValue(token) {
'class',
parseEntities(this.sliceSerialize(token), {attribute: true})
])
addShortcutPosition(this, 'class', token)
}

/**
Expand All @@ -476,6 +504,32 @@ function exitAttributeValue(token) {
list[list.length - 1][1] = parseEntities(this.sliceSerialize(token), {
attribute: true
})

const positions = this.data.directiveAttributePositions

if (positions) {
const position = positions[positions.length - 1]
position.end = point(token.end, 0)
position.value = {start: point(token.start, 0), end: point(token.end, 0)}
}
}

/**
* Handle the end of a quoted attribute value.
*
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitAttributeValueLiteral(token) {
const positions = this.data.directiveAttributePositions

if (positions) {
const position = positions[positions.length - 1]
position.end = point(token.end, 0)
// Exclude the quotes, which are always one character each.
// This also gives empty values (`a=""`) a place.
position.value = {start: point(token.start, 1), end: point(token.end, -1)}
}
}

/**
Expand All @@ -490,7 +544,18 @@ function exitAttributeName(token) {

// Attribute names in CommonMark are significantly limited, so character
// references can’t exist.
list.push([this.sliceSerialize(token), ''])
const name = this.sliceSerialize(token)
list.push([name, ''])

const positions = this.data.directiveAttributePositions

if (positions) {
positions.push({
name,
start: point(token.start, 0),
end: point(token.end, 0)
})
}
}

/**
Expand All @@ -516,7 +581,9 @@ function exitAttributes() {
}
}

const positions = this.data.directiveAttributePositions
this.data.directiveAttributes = undefined
this.data.directiveAttributePositions = undefined
this.resume() // Drop EOLs
const node = this.stack[this.stack.length - 1]
assert(
Expand All @@ -525,6 +592,51 @@ function exitAttributes() {
node.type === 'textDirective'
)
node.attributes = cleaned

if (positions) {
node.data = {...node.data, position: {attributes: positions}}
}
}

/**
* Track the positional info of an `id` or `class` shortcut.
*
* @param {CompileContext} context
* Compile context.
* @param {string} name
* Attribute name.
* @param {Token} token
* Value token (without the `#` or `.` marker).
* @returns {undefined}
* Nothing.
*/
function addShortcutPosition(context, name, token) {
const positions = context.data.directiveAttributePositions

if (positions) {
positions.push({
name,
// The marker (`#` or `.`) is always one character.
start: point(token.start, -1),
end: point(token.end, 0),
value: {start: point(token.start, 0), end: point(token.end, 0)}
})
}
}

/**
* Copy a micromark point to a unist point, moved by `delta` characters on its
* line.
*
* @param {Token['start']} d
* micromark point.
* @param {number} delta
* Characters to move by.
* @returns {Point}
* unist point.
*/
function point(d, delta) {
return {line: d.line, column: d.column + delta, offset: d.offset + delta}
}

/**
Expand Down
Loading
Loading