@moritzrs/micromark-extension-ofm-callout
Version:
micromark extension to support OFM callouts
291 lines (290 loc) • 7.56 kB
JavaScript
import { markdownSpace } from "micromark-util-character";
import { factorySpace } from "micromark-factory-space";
import { constants, types } from "micromark-util-symbol";
// ASCI Codes
const SPACE = 32;
const EXCLAMATION_MARK = 33;
const PLUS = 43;
const MINUS = 45;
const GREATER_THAN = 62;
const SQUARE_BRACKET_OPEN = 91;
const SQUARE_BRACKET_CLOSE = 93;
/**
* Create an extension for `micromark` to enable OFM callout syntax.
*/
export function ofmCallout() {
return {
document: {
[GREATER_THAN]: {
name: "ofmCallout",
tokenize: tokenize,
continuation: { tokenize: tokenizeContinuation },
exit: exit,
add: "before",
},
},
};
}
/**
* A tokenizer for Obsidian callout syntax.
*/
function tokenize(effects, ok, nok) {
return start;
/**
* Start of callout
*
* ```markdown
* > | > [!type]+ Title
* ^
* ```
*/
function start(code) {
effects.enter("ofmCallout", { _container: true });
effects.enter("ofmCalloutPrefix");
effects.consume(code);
return before_type;
}
/**
* Before callout type
*
* ```markdown
* > | > [!type]+ Title
* ^^
* ```
*/
function before_type(code) {
if (code === SPACE) {
effects.enter("ofmCalloutPrefixWhiteSpace");
effects.consume(code);
effects.exit("ofmCalloutPrefixWhiteSpace");
return type_whitespace;
}
if (code === SQUARE_BRACKET_OPEN) {
effects.enter("ofmCalloutType");
effects.enter("ofmCalloutTypeMarker");
effects.consume(code);
effects.exit("ofmCalloutTypeMarker");
return type_open;
}
return nok(code);
}
/**
* Inside whitespace before callout type
*
* ```markdown
* > | > [!type]+ Title
* ^
* ```
*/
function type_whitespace(code) {
if (code !== SQUARE_BRACKET_OPEN)
return nok(code);
effects.enter("ofmCalloutType");
effects.enter("ofmCalloutTypeMarker");
effects.consume(code);
effects.exit("ofmCalloutTypeMarker");
return type_open;
}
/**
* After callout type marker
*
* ```markdown
* > | > [!type]+ Title
* ^
* ```
*/
function type_open(code) {
if (code !== EXCLAMATION_MARK)
return nok(code);
effects.enter("ofmCalloutTypeMark");
effects.consume(code);
effects.exit("ofmCalloutTypeMark");
return type_marker;
}
/**
* After callout type marker
*
* ```markdown
* > | > [!type]+ Title
* ^
* ```
*/
function type_marker(code) {
if (!code || code < SPACE || code === SQUARE_BRACKET_CLOSE)
return nok(code);
effects.enter("ofmCalloutTypeValue");
effects.consume(code);
return type_content;
}
/**
* Inside callout type declaration
*
* ```markdown
* > | > [!type]+ Title
* ^^^^
* ```
*/
function type_content(code) {
if (!code || code < SPACE)
return nok(code);
if (code === SQUARE_BRACKET_CLOSE) {
effects.exit("ofmCalloutTypeValue");
effects.enter("ofmCalloutTypeMarker");
effects.consume(code);
effects.exit("ofmCalloutTypeMarker");
return type_close;
}
effects.consume(code);
return type_content;
}
/**
* After callout type closing marker
*
* ```markdown
* > | > [!type]+ Title
* ^
* ```
*/
function type_close(code) {
if (code === PLUS || code === MINUS) {
effects.enter("ofmCalloutFoldable");
effects.consume(code);
effects.exit("ofmCalloutFoldable");
effects.exit("ofmCalloutType");
return type_foldable;
}
if (code === SPACE) {
effects.exit("ofmCalloutType");
effects.enter("ofmCalloutPrefixWhiteSpace");
effects.consume(code);
effects.exit("ofmCalloutPrefixWhiteSpace");
return before_title;
}
if (!code || code < SPACE) {
effects.exit("ofmCalloutType");
effects.exit("ofmCalloutPrefix");
return ok(code);
}
return nok(code);
}
/**
* After callout foldable marker
*
* ```markdown
* > | > [!type]+ Title
* ^
* ```
*/
function type_foldable(code) {
if (code === SPACE) {
effects.enter("ofmCalloutPrefixWhiteSpace");
effects.consume(code);
effects.exit("ofmCalloutPrefixWhiteSpace");
return before_title;
}
if (!code || code < SPACE) {
effects.exit("ofmCalloutPrefix");
return ok(code);
}
return nok(code);
}
/**
* Before callout title
*
* ```markdown
* > | > [!type]+ Title
* ^^
* ```
*/
function before_title(code) {
if (!code || code < SPACE) {
effects.exit("ofmCalloutPrefix");
return ok(code);
}
effects.enter("ofmCalloutTitle");
effects.consume(code);
return title;
}
/**
* Inside callout title
*
* ```markdown
* > | > [!type]+ Title
* ^^^^^
* ```
*/
function title(code) {
if (!code || code < SPACE) {
effects.exit("ofmCalloutTitle");
effects.exit("ofmCalloutPrefix");
return ok(code);
}
effects.consume(code);
return title;
}
}
function tokenizeContinuation(effects, ok, nok) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
return start;
/**
* Start of callout continuation
*
* ```markdown
* > | > Content
* ^
* ```
*/
function start(code) {
const state = self.containerState;
if (!state.calloutContent)
effects.enter("ofmCalloutContent");
if (markdownSpace(code)) {
return factorySpace(effects, before_content, types.linePrefix, self.parser.constructs.disable.null?.includes("codeIndented")
? undefined
: constants.tabSize)(code);
}
return before_content(code);
}
/**
* Before block quote marker
*
* ```markdown
* > | > Content
* ^
* ```
*/
function before_content(code) {
if (code === GREATER_THAN) {
effects.enter("ofmCalloutContentPrefix");
effects.consume(code);
return after_content;
}
return nok(code);
}
/**
* After block quote marker
*
* ```markdown
* > | > Content
* ^
* ```
*/
function after_content(code) {
const state = self.containerState;
if (state)
state.calloutContent = true;
effects.exit("ofmCalloutContentPrefix");
return ok(code);
}
}
function exit(effects) {
const state = this.containerState;
if (state?.calloutContent) {
effects.exit("ofmCalloutContent");
}
if (state)
state.calloutContent = false;
effects.exit("ofmCallout");
return undefined;
}