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
}
}
+3 -1
View File
@@ -10,6 +10,7 @@ import remarkMath from "remark-math"
// @ts-ignore
import remarkAbbr from "remark-abbr"
import remarkFootnotes from 'remark-footnotes'
import remarkCallouts from "remark-callouts";
import rehypeKatexSvelte from 'rehype-katex-svelte';
// import github from "remark-github";
@@ -343,6 +344,8 @@ const config = {
remarkMath,
remarkAbbr,
[remarkFootnotes, { inlineNotes: true }],
remarkGfm,
[remarkCallouts, {}],
[remarkWikiLink, {
// @ts-ignore
aliasDivider: "|",
@@ -353,7 +356,6 @@ const config = {
// wikiLinkClassName,
// newClassName,
}],
remarkGfm,
// [citePlugin, {
// syntax: {
// // see micromark-extension-cite
+2
View File
@@ -33,6 +33,7 @@
"rehype-katex-svelte": "1.2",
"rehype-slug": "^6.0.0",
"remark-abbr": "^1.4.2",
"remark-callouts": "workspace:^",
"remark-footnotes": "2",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.0",
@@ -74,6 +75,7 @@
"@octokit/types": "^13.5.0",
"@resvg/resvg-js": "^2.6.2",
"@steeze-ui/svelte-icon": "^1.5.0",
"@tabler/icons-svelte": "^3.11.0",
"@tusbar/cache-control": "^1.0.2",
"@uiw/codemirror-theme-github": "^4.22.2",
"Notes": "file:Notes-1.0.0.tgz",
+27
View File
@@ -0,0 +1,27 @@
<script lang="ts">
export let calloutType: string;
import { IconExclamationCircle } from "@tabler/icons-svelte";
</script>
<div class="callout" data-callout={calloutType}>
<div class="callout-title">
{#if $$slots.icon}
<div class="callout-icon"><slot name="icon" /></div>
{:else}
<div class="callout-icon"><IconExclamationCircle /></div>
{/if}
<div class="callout-title-inner">
<slot name="title"
>{calloutType.replace(/\w\S*/g, function (txt) {
return (
txt.charAt(0).toUpperCase() +
txt.substring(1).toLowerCase()
);
})}</slot
>
</div>
</div>
{#if $$slots.body}
<div class="callout-body"><slot name="body" /></div>
{/if}
</div>
@@ -1,6 +1,7 @@
<script context="module">
import img from "$lib/htmlComponents/img.svelte";
export { img };
import Callout from "$lib/Callout.svelte";
export { img, Callout };
</script>
<slot />
+194
View File
@@ -0,0 +1,194 @@
:root {
--callout-bug: var(--color-red);
--callout-default: var(--color-blue);
--callout-error: var(--color-red);
--callout-example: var(--color-purple);
--callout-fail: var(--color-red);
--callout-important: var(--color-cyan);
--callout-info: var(--color-blue);
--callout-question: var(--color-orange);
--callout-success: var(--color-green);
--callout-summary: var(--color-cyan);
--callout-tip: var(--color-cyan);
--callout-todo: var(--color-blue);
--callout-warning: var(--color-orange);
--callout-quote: rgb(158, 158, 158);
--callout-title-weight: bold;
--callout-radius: var(--border-radius);
--callout-border-opacity: 25%;
--callout-background-opacity: 10%;
--callout-content-padding: 0;
--callout-content-background: transparent;
}
.callout {
--callout-color: var(--callout-default);
/* --callout-icon: lucide-pencil; */
}
.callout[data-callout="abstract"],
.callout[data-callout="summary"],
.callout[data-callout="tldr"] {
--callout-color: var(--callout-summary);
/* --callout-icon: lucide-clipboard-list; */
}
.callout[data-callout="info"] {
--callout-color: var(--callout-info);
/* --callout-icon: lucide-info; */
}
.callout[data-callout="todo"] {
--callout-color: var(--callout-todo);
/* --callout-icon: lucide-check-circle-2; */
}
.callout[data-callout="important"] {
--callout-color: var(--callout-important);
/* --callout-icon: lucide-flame; */
}
.callout[data-callout="tip"],
.callout[data-callout="hint"] {
--callout-color: var(--callout-tip);
/* --callout-icon: lucide-flame; */
}
.callout[data-callout="success"],
.callout[data-callout="check"],
.callout[data-callout="done"] {
--callout-color: var(--callout-success);
/* --callout-icon: lucide-check; */
}
.callout[data-callout="question"],
.callout[data-callout="help"],
.callout[data-callout="faq"] {
--callout-color: var(--callout-question);
/* --callout-icon: help-circle; */
}
.callout[data-callout="warning"],
.callout[data-callout="caution"],
.callout[data-callout="attention"] {
--callout-color: var(--callout-warning);
/* --callout-icon: lucide-alert-triangle; */
}
.callout[data-callout="failure"],
.callout[data-callout="fail"],
.callout[data-callout="missing"] {
--callout-color: var(--callout-fail);
/* --callout-icon: lucide-x; */
}
.callout[data-callout="danger"],
.callout[data-callout="error"] {
--callout-color: var(--callout-error);
/* --callout-icon: lucide-zap; */
}
.callout[data-callout="bug"] {
--callout-color: var(--callout-bug);
/* --callout-icon: lucide-bug; */
}
.callout[data-callout="example"] {
--callout-color: var(--callout-example);
/* --callout-icon: lucide-list; */
}
.callout[data-callout="quote"],
.callout[data-callout="cite"] {
--callout-color: var(--callout-quote);
/* --callout-icon: quote-glyph; */
}
.callout {
overflow: hidden;
border-style: solid;
border-color: color-mix(in srgb, var(--callout-color) var(--callout-border-opacity), transparent);
border-width: var(--callout-border-width);
border-radius: var(--callout-radius);
margin-block: 1em;
margin-inline: 0;
mix-blend-mode: var(--callout-blend-mode);
background-color: color-mix(in srgb, var(--callout-color) var(--callout-background-opacity), transparent);
padding: 0.75em;
padding-inline-start: 1.5em;
--callout-title-color: var(--callout-color);
}
.callout.is-collapsible .callout-title {
cursor: var(--cursor);
}
.callout-title {
padding: var(--callout-title-padding);
display: flex;
gap: calc(var(--spacing)/4);
font-size: var(--callout-title-size);
color: rgb(var(--callout-color));
line-height: var(--line-height-tight);
align-items: flex-start;
}
.callout-title-inner {
--font-weight: var(--callout-title-weight);
font-weight: var(--font-weight);
color: var(--callout-title-color);
}
/* .callout-title {
--font-weight: var(--callout-title-weight);
font-weight: var(--font-weight);
color: var(--callout-title-color);
padding: var(--callout-title-padding);
font-size: var(--callout-title-size);
line-height: var(--line-height-tight);
} */
.callout-content {
overflow-block: auto;
padding: var(--callout-content-padding);
background-color: var(--callout-content-background);
}
.callout-content .callout {
margin-block-start: 20px;
}
.callout-icon {
flex: 0 0 auto;
display: flex;
align-items: center;
}
.callout-icon .tabler-icon {
color: var(--callout-color);
}
.callout-icon::after {
content: "\200B";
}
/* .callout-fold {
display: flex;
align-items: center;
padding-inline-end: var(--size-4-2);
}
.callout-fold::after {
content: "\200B";
}
.callout-fold .svg-icon {
transition: transform 100ms ease-in-out;
}
.callout-fold.is-collapsed .svg-icon {
transform: rotate(calc(var(--direction) * -1 * 90deg));
} */
+20 -1
View File
@@ -1,4 +1,5 @@
@import "./style/code.css";
@import "./style/callout.css";
@import "./style/footnote.css";
:root {
@@ -32,6 +33,15 @@
--code-background-color: #0d1117;
--code-color: #fff;
--color-red: #e93147;
--color-orange: #ec7500;
--color-yellow: #e0ac00;
--color-green: #08b94e;
--color-cyan: #00bfbc;
--color-blue: #086ddd;
--color-purple: #7852ee;
--color-pink: #d53984;
scroll-behavior: smooth;
}
@@ -49,7 +59,16 @@
--input-color: #d8d8d8;
--font-color: rgba(255, 255, 255, .87);
--font-color-contrast: rgba(0, 0, 0, .87);
--font-color-secondary: rgba(255, 255, 255, .6)
--font-color-secondary: rgba(255, 255, 255, .6);
--color-red: #fb464c;
--color-orange: #e9973f;
--color-yellow: #e0de71;
--color-green: #44cf6e;
--color-cyan: #53dfdd;
--color-blue: #027aff;
--color-purple: #a882ff;
--color-pink: #fa99cd;
}
}