Skip to content

Commit

Permalink
[#69675] Support figure-md directive
Browse files Browse the repository at this point in the history
  • Loading branch information
Trzcin authored and mgielda committed Dec 3, 2024
1 parent 8b682f0 commit d93fef7
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src/components/Preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,11 @@ const Preview = styled.div`
margin-right: 8px;
transform: translateY(2px);
}
figcaption {
text-align: center;
margin-top: 12px;
}
`;
Preview.defaultProps = { className: "myst-preview" };

Expand Down
138 changes: 138 additions & 0 deletions src/hooks/markdownFigureMd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// https://github.com/executablebooks/markdown-it-docutils/blob/main/src/directives/images.ts
// figure-md seems to be a myst-parser (MyST+Sphinx) thing but the MyST project seems to be
// evolving away from Sphinx towards mystmd, so slim chance of mainlining this
import { directiveOptions, directivesDefault } from "markdown-it-docutils";

const shared_option_spec = {
alt: directiveOptions.unchanged,
height: directiveOptions.length_or_unitless,
width: directiveOptions.length_or_percentage_or_unitless,
scale: directiveOptions.percentage,
target: directiveOptions.unchanged_required,
class: directiveOptions.class_option,
name: directiveOptions.unchanged,
};

export class FigureMd extends directivesDefault.image {
option_spec = {
...shared_option_spec,
align: directiveOptions.create_choice(["left", "center", "right"]),
figwidth: directiveOptions.length_or_percentage_or_unitless_figure,
figclass: directiveOptions.class_option,
};
has_content = true;
required_arguments = 0;
optional_arguments = 1;
run(data) {
const openToken = this.createToken("figure_open", "figure", 1, {
map: data.map,
block: true,
});
if (data.options.figclass) {
openToken.attrJoin("class", data.options.figclass.join(" "));
}
if (data.options.align) {
openToken.attrJoin("class", `align-${data.options.align}`);
}
if (data.options.figwidth && data.options.figwidth !== "image") {
openToken.attrSet("width", data.options.figwidth);
}
let target;
if (data.args.length > 0) {
target = newTarget(this.state, openToken, "fig", data.args[0], data.body.trim());
openToken.attrJoin("class", "numbered");
}

let captionTokens = [];
let legendTokens = [];
let imageToken = null;
if (data.body) {
imageToken = this.state.md.parseInline(data.body.split("\n")[0], this.state.env)[0].children[0];
imageToken.map = data.map;
if (data.options.height) {
imageToken.attrSet("height", data.options.height);
}
if (data.options.width) {
imageToken.attrSet("width", data.options.width);
}
if (data.options.align) {
imageToken.attrJoin("class", `align-${data.options.align}`);
}
if (data.options.class) {
imageToken.attrJoin("class", data.options.class.join(" "));
}

const [caption, ...legendParts] = data.body.split("\n\n").slice(1);
const legend = legendParts.join("\n\n");
const captionMap = data.bodyMap[0] + 2;
const openCaption = this.createToken("figure_caption_open", "figcaption", 1, {
block: true,
});
if (target) {
openCaption.attrSet("number", `${target.number}`);
}
const captionBody = this.nestedParse(caption, captionMap);
const closeCaption = this.createToken("figure_caption_close", "figcaption", -1, {
block: true,
});
captionTokens = [openCaption, ...captionBody, closeCaption];
if (legend) {
const legendMap = captionMap + caption.split("\n").length + 1;
const openLegend = this.createToken("figure_legend_open", "", 1, {
block: true,
});
const legendBody = this.nestedParse(legend, legendMap);
const closeLegend = this.createToken("figure_legend_close", "", -1, {
block: true,
});
legendTokens = [openLegend, ...legendBody, closeLegend];
}
}
const closeToken = this.createToken("figure_close", "figure", -1, { block: true });
return [openToken, imageToken, ...captionTokens, ...legendTokens, closeToken];
}
}

function newTarget(state, token, kind, label, title, silent = false) {
const env = getDocState(state);
const number = nextNumber(state, kind);
const target = {
label,
kind,
number,
title,
};
if (!silent) {
const meta = getNamespacedMeta(token);
meta.target = target;
token.attrSet("id", label);
env.targets[label] = target;
}
return target;
}

function getDocState(state) {
const env = state.env?.docutils ?? {};
if (!env.targets) env.targets = {};
if (!env.references) env.references = [];
if (!env.numbering) env.numbering = {};
if (!state.env.docutils) state.env.docutils = env;
return env;
}

function nextNumber(state, kind) {
const env = getDocState(state);
if (env.numbering[kind] == null) {
env.numbering[kind] = 1;
} else {
env.numbering[kind] += 1;
}
return env.numbering[kind];
}

function getNamespacedMeta(token) {
const meta = token.meta?.docutils ?? {};
if (!token.meta) token.meta = {};
if (!token.meta.docutils) token.meta.docutils = meta;
return meta;
}
5 changes: 3 additions & 2 deletions src/hooks/useText.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import markdownitDocutils from "markdown-it-docutils";
import markdownitDocutils, { directivesDefault } from "markdown-it-docutils";
import purify from "dompurify";
import markdownIt from "markdown-it";
import { markdownReplacer, useCustomRoles } from "./markdownReplacer";
Expand All @@ -15,6 +15,7 @@ import { useComputed } from "@preact/signals";
import markdownCheckboxes from "markdown-it-checkbox";
import { colonFencedBlocks } from "./markdownFence";
import { markdownItMapUrls } from "./markdownUrlMapping";
import { FigureMd } from "./markdownFigureMd";

const countOccurences = (str, pattern) => (str?.match(pattern) || []).length;

Expand Down Expand Up @@ -106,7 +107,7 @@ export const useText = ({ preview }) => {

const markdown = useComputed(() => {
const md = markdownIt({ breaks: true, linkify: true })
.use(markdownitDocutils)
.use(markdownitDocutils, { directives: { ...directivesDefault, "figure-md": FigureMd } })
.use(markdownReplacer(options.transforms.value, options.parent, cache.transform))
.use(useCustomRoles(options.customRoles.value, options.parent, cache.transform))
.use(markdownMermaid, { lineMap, parent: options.parent })
Expand Down

0 comments on commit d93fef7

Please sign in to comment.