Add callouts support

Adds a colour palette
Adds homemade remark-callouts package
Adds tabler icons dependency
This commit is contained in:
Jade Ellis
2024-07-30 18:30:15 +01:00
parent e7e311b080
commit f1977924f8
13 changed files with 1307 additions and 4 deletions
+2
View File
@@ -0,0 +1,2 @@
dist
node_modules
+42
View File
@@ -0,0 +1,42 @@
{
"name": "remark-callouts",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "dist/index.esm.js",
"exports": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js",
"types": "./dist/index.d.ts"
},
"types": "dist/index.d.ts",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.6",
"@types/mdast": "^4.0.4",
"@types/node": "^20.14.2",
"@types/svg-parser": "^2.0.6",
"@types/trim": "^0.1.3",
"@types/unist": "^3.0.2",
"mdast": "^3.0.0",
"rollup": "^4.18.0",
"rollup-plugin-dts": "^6.1.1",
"unified": "^6.2.0"
},
"files": [
"dist"
],
"dependencies": {
"mdast-util-from-markdown": "^1.3.1",
"svg-parser": "^2.0.4",
"trim": "^0.0.1",
"unist-util-visit": "^4.1.2"
}
}
+20
View File
@@ -0,0 +1,20 @@
// rollup.config.js
import typescript from '@rollup/plugin-typescript';
import { dts } from "rollup-plugin-dts";
import pkg from './package.json' with { type: "json" };
export default [
{
input: 'src/index.ts',
output: [
{ file: pkg.exports.require, format: 'cjs' },
{ file: pkg.exports.import, format: 'es' }
],
plugins: [typescript()]
},
{
input: 'src/index.ts',
output: [{ file: pkg.exports.types, format: 'es' }],
plugins: [dts()],
}
];
+326
View File
@@ -0,0 +1,326 @@
import { visit } from "unist-util-visit";
import { fromMarkdown } from "mdast-util-from-markdown";
// @ts-ignore
import type { Plugin } from "unified";
import type { Node, Data, Parent } from "unist";
import type { Blockquote, Heading, Text, BlockContent } from "mdast";
import { parse } from "svg-parser";
// import { calloutTypes } from "./calloutTypes";
// escape regex special characters
function escapeRegExp(s: string) {
return s.replace(new RegExp(`[-[\\]{}()*+?.\\\\^$|/]`, "g"), "\\$&");
}
import tok from "./tokeniseCallout"
function trim(str: string){
return str.replace(/^\s*|\s*$/g, '');
}
export interface Config {
// classNameMaps: {
// block: ClassNameMap;
// title: ClassNameMap;
// };
// dataMaps: {
// block: (data: Data) => Data;
// title: (data: Data) => Data;
// };
// types: { [index: string]: string | object };
}
export const defaultConfig: Config = {
// classNameMaps: {
// block: "callout",
// title: "callout-title",
// },
// dataMaps: {
// block: (data) => data,
// title: (data) => data,
// },
// types: { ...calloutTypes },
};
type ClassNames = string | string[];
type ClassNameMap = ClassNames | ((title: string) => ClassNames);
function formatClassNameMap(gen: ClassNameMap) {
return (title: string) => {
const classNames = typeof gen == "function" ? gen(title) : gen;
return typeof classNames == "object" ? classNames.join(" ") : classNames;
};
}
export const callouts: Plugin = function (providedConfig?: Partial<Config>) {
const config: Config = { ...defaultConfig, ...providedConfig };
// const defaultKeywords: string = Object.keys(config.types)
// .map(escapeRegExp)
// .join("|");
// @ts-ignore
const Parser = this.Parser
console.log(Parser)
const blockTokenizers = Parser.prototype.blockTokenizers
const blockMethods = Parser.prototype.blockMethods
console.log(blockMethods, blockTokenizers)
blockTokenizers.callout = tok
// console.log(blockTokenizers.blockquote.toString())
var insertPoint = blockMethods.indexOf('fencedCode') + 1
blockMethods.splice(insertPoint, 0, 'callout')
// @ts-ignore
const Compiler = this.Compiler
if (Compiler != null) {
const visitors = Compiler.prototype.visitors
if (visitors) {
visitors.callout = function (node: { keyword: string; title: string; }) {
var values = this.block(node).split('\n');
var result = [];
var length = values.length;
var index = -1;
var value;
while (++index < length) {
value = values[index];
result[index] = (value ? ' ' : '') + value;
}
return '> [!'+node.keyword+'] ' + node.title + '\n' + result.join('\n>');
}
}
}
// return function (tree) {
// visit(tree, (node: Node, index, parent: Parent) => {
// // Filter required elems
// if (node.type !== "blockquote") return;
// /** add breaks to text without needing spaces or escapes (turns enters into <br>)
// * code taken directly from remark-breaks,
// * see https://github.com/remarkjs/remark-breaks for more info on what this does.
// */
// // visit(node, "text", (node: Text, index: number, parent: Parent) => {
// // const result = [];
// // let start = 0;
// // find.lastIndex = 0;
// // let match = find.exec(node.value);
// // while (match) {
// // const position = match.index;
// // if (start !== position) {
// // result.push({
// // type: "text",
// // value: node.value.slice(start, position),
// // });
// // }
// // result.push({ type: "break" });
// // start = position + match[0].length;
// // match = find.exec(node.value);
// // }
// // if (result.length > 0 && parent && typeof index === "number") {
// // if (start < node.value.length) {
// // result.push({ type: "text", value: node.value.slice(start) });
// // }
// // parent.children.splice(index, 1, ...result);
// // return index + result.length;
// // }
// // });
// /** add classnames to headings within blockquotes,
// * mainly to identify when using other plugins that
// * might interfere. for eg, rehype-auto-link-headings.
// */
// visit(node, "heading", (node) => {
// const heading = node as Heading;
// heading.data = {
// hProperties: {
// className: "blockquote-heading",
// },
// };
// });
// // wrap blockquote in a div
// const wrapper = {
// ...node,
// type: "element",
// tagName: "div",
// data: {
// hProperties: {},
// },
// children: [node],
// };
// parent.children.splice(Number(index), 1, wrapper);
// const blockquote = wrapper.children[0] as Blockquote;
// blockquote.data = {
// hProperties: {
// className: "blockquote",
// },
// };
// console.log(blockquote)
// // check for callout syntax starts here
// if (
// blockquote.children.length <= 0 ||
// blockquote.children[0].type !== "paragraph"
// )
// return;
// const paragraph = blockquote.children[0];
// console.log(paragraph)
// if (
// paragraph.children.length <= 0 ||
// paragraph.children[0].type !== "text"
// )
// return;
// const [t, ...rest] = paragraph.children;
// const regex = new RegExp(
// `^\\[!(?<keyword>(.*?))\\][\t\f ]?(?<title>.*?)$`,
// "gi"
// );
// const m = regex.exec(t.value);
// // if no callout syntax, forget about it.
// if (!m) return;
// const [key, title] = [m.groups?.keyword, m.groups?.title];
// // if there's nothing inside the brackets, is it really a callout ?
// if (!key) return;
// const keyword = key.toLowerCase();
// // const isOneOfKeywords: boolean = new RegExp(defaultKeywords).test(
// // keyword
// // );
// if (title) {
// const mdast = fromMarkdown(title.trim()).children[0];
// if (mdast.type === "heading") {
// mdast.data = {
// ...mdast.data,
// hProperties: {
// className: "blockquote-heading",
// },
// };
// }
// blockquote.children.unshift(mdast as BlockContent);
// } else {
// t.value =
// typeof keyword.charAt(0) === "string"
// ? keyword.charAt(0).toUpperCase() + keyword.slice(1)
// : keyword;
// }
// const entry: { [index: string]: string } = {};
// // if (isOneOfKeywords) {
// // if (typeof config?.types[keyword] === "string") {
// // const e = String(config?.types[keyword]);
// // Object.assign(entry, config?.types[e]);
// // } else {
// // Object.assign(entry, config?.types[keyword]);
// // }
// // } else {
// // Object.assign(entry, config?.types["note"]);
// // }
// let parsedSvg;
// if (entry && entry.svg) {
// parsedSvg = parse(entry.svg);
// }
// // create icon and title node wrapped in div
// const titleNode: object = {
// type: "element",
// children: [
// {
// type: "element",
// tagName: "span",
// data: {
// hName: "span",
// hProperties: {
// style: `color:${entry?.color}`,
// className: "callout-icon",
// },
// hChildren: parsedSvg?.children ? parsedSvg.children : [],
// },
// },
// {
// type: "element",
// children: title ? [blockquote.children[0]] : [t],
// data: {
// hName: "strong",
// },
// },
// ],
// data: {
// ...blockquote.children[0]?.data,
// hProperties: {
// className: `${formatClassNameMap(config.classNameMaps.title)(
// keyword
// )} ${"note"}`,
// style: `background-color: ${entry?.color}1a;`,
// },
// },
// };
// // remove the callout paragraph from the content body
// if (title) {
// blockquote.children.shift();
// }
// if (rest.length > 0) {
// rest.shift();
// paragraph.children = rest;
// } else {
// blockquote.children.shift();
// }
// // wrap blockquote content in div
// const contentNode: object = {
// type: "element",
// children: blockquote.children,
// data: {
// hProperties: {
// className: "callout-content",
// style:
// parent.type !== "root"
// ? `border-right:1px solid ${entry?.color}33;
// border-bottom:1px solid ${entry?.color}33;`
// : "",
// },
// },
// };
// if (blockquote.children.length > 0)
// blockquote.children = [contentNode] as BlockContent[];
// blockquote.children.unshift(titleNode as BlockContent);
// // Add classes for the callout block
// blockquote.data = config.dataMaps.block({
// ...blockquote.data,
// hProperties: {
// className: formatClassNameMap(config.classNameMaps.block)(
// keyword.toLowerCase()
// ),
// style: `border-left-color:${entry?.color};`,
// },
// });
// });
// };
};
export default callouts;
@@ -0,0 +1,245 @@
'use strict';
import trim from "trim";
export default blockquote;
var C_NEWLINE = '\n';
var C_TAB = '\t';
var C_SPACE = ' ';
var C_GT = '>';
// TODO:
// - Grow/shrink support
// - Customise AST output
/* Tokenise a blockquote. */
function blockquote(eat: { (arg0: string): any; (arg0: string): any; now: any; }, value: string, silent: boolean) {
var self = this;
var offsets = self.offset;
var tokenizers = self.blockTokenizers;
var interruptors = [
['indentedCode', { commonmark: true }],
['fencedCode', { commonmark: true }],
['atxHeading', { commonmark: true }],
['setextHeading', { commonmark: true }],
['thematicBreak', { commonmark: true }],
['html', { commonmark: true }],
['list', { commonmark: true }],
['definition', { commonmark: false }],
['footnote', { commonmark: false }]
];
var now = eat.now();
var currentLine = now.line;
var length = value.length;
var values = [];
var contents = [];
var indents = [];
var add;
var index = 0;
var character;
var rest;
var nextIndex;
var content;
var line;
var startIndex;
var prefixed;
var exit;
while (index < length) {
character = value.charAt(index);
if (character !== C_SPACE && character !== C_TAB) {
break;
}
index++;
}
if (value.charAt(index) !== C_GT) {
return;
}
const regex = /^>\s*\[!(?<keyword>(.*?))\][\t\f ]?(?<title>.*?)$/i;
nextIndex = value.indexOf(C_NEWLINE, index);
content = value.slice(index, nextIndex);
const m = regex.exec(content); // value.slice(index)
if (!m) {
return;
}
if (!m.groups?.keyword) return;
if (silent) {
return true;
}
index = 0;
let titleLine = true
while (index < length) {
nextIndex = value.indexOf(C_NEWLINE, index);
startIndex = index;
prefixed = false;
if (nextIndex === -1) {
nextIndex = length;
}
while (index < length) {
character = value.charAt(index);
if (character !== C_SPACE && character !== C_TAB) {
break;
}
index++;
}
if (value.charAt(index) === C_GT) {
index++;
prefixed = true;
if (value.charAt(index) === C_SPACE) {
index++;
}
} else {
index = startIndex;
}
// regex.lastIndex = index
content = value.slice(index, nextIndex);
if (!prefixed && !trim(content)) {
index = startIndex;
break;
}
if (!prefixed) {
rest = value.slice(index);
/* Check if the following code contains a possible
* block. */
if (interrupt(interruptors, tokenizers, self, [eat, rest, true])) {
break;
}
}
line = startIndex === index ? content : value.slice(startIndex, nextIndex);
indents.push(index - startIndex);
values.push(line);
if (titleLine) {
titleLine = false
} else {
contents.push(content);
}
index = nextIndex + 1;
}
index = -1;
length = indents.length;
add = eat(values.join(C_NEWLINE));
while (++index < length) {
offsets[currentLine] = (offsets[currentLine] || 0) + indents[index];
currentLine++;
}
exit = self.enterBlock();
const title = self.tokenizeInline(m.groups?.title, now);
contents = self.tokenizeBlock(contents.join(C_NEWLINE), now);
exit();
console.log(title,)
return add({
type: 'callout',
children: [{
type: "heading",
children: title,
data: { hName: 'svelte:fragment',
hProperties: {
slot: "title"
} }
}, {
type: "block",
children: contents,
data: { hName: 'svelte:fragment',
hProperties: {
slot: "body"
} }
}],
keyword: m.groups?.keyword,
data: {
hName: 'Components.Callout',
hProperties: {
"calloutType": m.groups?.keyword
},
}
});
// return add({
// type: 'callout',
// children: [{
// type: "heading",
// children: title,
// data: { hName: 'div',
// hProperties: {
// className: "callout-title"
// } }
// }, {
// type: "block",
// children: contents,
// data: { hName: 'div',
// hProperties: {
// className: "callout-content"
// } }
// }],
// keyword: m.groups?.keyword,
// data: {
// hName: 'div',
// hProperties: {
// className: "callout",
// "data-callout": m.groups?.keyword
// },
// }
// });
}
function interrupt(interruptors, tokenizers, ctx, params) {
var bools = ['pedantic', 'commonmark'];
var count = bools.length;
var length = interruptors.length;
var index = -1;
var interruptor;
var config;
var fn;
var offset;
var bool;
var ignore;
while (++index < length) {
interruptor = interruptors[index];
config = interruptor[1] || {};
fn = interruptor[0];
offset = -1;
ignore = false;
while (++offset < count) {
bool = bools[offset];
if (config[bool] !== undefined && config[bool] !== ctx.options[bool]) {
ignore = true;
break;
}
}
if (ignore) {
continue;
}
if (tokenizers[fn].apply(ctx, params)) {
return true;
}
}
return false;
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"skipLibCheck": true,
"noImplicitAny": true,
"allowJs": true,
"noEmit": true,
"resolveJsonModule": true
}
}