Skip to content
Merged
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@ pnpm-debug.log
mume
.crossnote
crossnote
styles/**/*.css
docs
173 changes: 173 additions & 0 deletions src/custom-markdown-it-features/callout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import MarkdownIt from 'markdown-it';
import Token from 'markdown-it/lib/token';

export default (md: MarkdownIt) => {
const _calloutTypes = new Set([
'note',
'summary',
'abstract',
Comment on lines +4 to +8
'tldr',
'info',
'todo',
'hint',
'tip',
'important',
'check',
'done',
'success',
'help',
'question',
'faq',
'attention',
'caution',
'warning',
'fail',
'failure',
'missing',
'danger',
'error',
'bug',
'example',
'cite',
'quote',
]);
const _calloutTitleMap: Record<string, string> = {
note: 'Note',
summary: 'Summary',
abstract: 'Abstract',
tldr: 'TL;DR',
info: 'Info',
todo: 'Todo',
hint: 'Hint',
tip: 'Tip',
important: 'Important',
check: 'Check',
done: 'Done',
success: 'Success',
help: 'Help',
question: 'Question',
faq: 'FAQ',
attention: 'Attention',
caution: 'Caution',
warning: 'Warning',
fail: 'Fail',
failure: 'Failure',
missing: 'Missing',
danger: 'Danger',
error: 'Error',
bug: 'Bug',
example: 'Example',
cite: 'Cite',
quote: 'Quote',
};

// replace blockquote renderer
const originalBlockquoteOpen =
md.renderer.rules.blockquote_open || (() => '<blockquote>\n');
const originalBlockquoteClose =
md.renderer.rules.blockquote_close || (() => '</blockquote>\n');

// find the matching blockquote_close token and mark it
const markCalloutCloseToken = (
tokens: Token[],
openIdx: number,
calloutTag: 'div' | 'details',
) => {
const openToken = tokens[openIdx];
const targetLevel = openToken.level;
for (let i = openIdx + 1; i < tokens.length; i += 1) {
const token = tokens[i];
if (token.type === 'blockquote_close' && token.level === targetLevel) {
token.meta = { ...(token.meta || {}), callout: true, calloutTag };
return;
}
}
};

md.renderer.rules.blockquote_open = (tokens, idx, options, env, self) => {
const token = tokens[idx];
// check if it's a callout: see if the first child token is a paragraph and starts with [!
let isCallout = false;
let calloutType = 'info';
let title = '';
let foldable: 'open' | 'closed' | null = null;

// find the next non-empty token
const nextToken = tokens[idx + 1];
if (nextToken && nextToken.type === 'paragraph_open') {
const textToken = tokens[idx + 2]; // inline token
if (textToken && textToken.type === 'inline') {
const content = textToken.content;

// use regex to match the callout pattern: [!type]+ optional title
const match = content.match(
/^\[!(\w+)\]([+-])?(?:[ \t]+([^\r\n]+))?(?:\r?\n|$)/,
);
if (match && _calloutTypes.has(match[1].toLowerCase())) {
isCallout = true;
calloutType = match[1].toLowerCase();
foldable =
match[2] === '+' ? 'open' : match[2] === '-' ? 'closed' : null;
title = match[3] || '';

// remove the callout marker and "\n" from the inline token's children
textToken.children =
textToken.children?.filter((_, i) => i > 1) || null;

Comment on lines +113 to +116
const remainingChildren = textToken.children;
const isEmptyParagraph =
!remainingChildren ||
remainingChildren.length === 0 ||
remainingChildren.every(
(child) =>
child.type === 'softbreak' || child.type === 'hardbreak',
);

if (isEmptyParagraph) {
const paragraphOpen = tokens[idx + 1];
const paragraphClose = tokens[idx + 3];
if (paragraphOpen) {
paragraphOpen.hidden = true;
}
textToken.hidden = true;
if (paragraphClose) {
paragraphClose.hidden = true;
}
}
}
}
} else {
isCallout = false;
}

if (isCallout) {
const calloutTag = foldable ? 'details' : 'div';
token.meta = { callout: true, type: calloutType, title, calloutTag };
markCalloutCloseToken(tokens, idx, calloutTag);
const displayTitle = title || _calloutTitleMap[calloutType];
if (foldable) {
const openAttr = foldable === 'open' ? ' open' : '';
let html = `<details class="callout" data-callout="${calloutType}"${openAttr}>\n`;
html += `<summary class="callout-title">${md.utils.escapeHtml(displayTitle)}</summary>\n`;
return html;
}

let html = `<div class="callout" data-callout="${calloutType}">\n`;
if (displayTitle) {
html += `<div class="callout-title">${md.utils.escapeHtml(displayTitle)}</div>\n`;
}
return html;
} else {
return originalBlockquoteOpen(tokens, idx, options, env, self);
}
};

md.renderer.rules.blockquote_close = (tokens, idx, options, env, self) => {
if (tokens[idx].meta?.callout) {
return tokens[idx].meta?.calloutTag === 'details'
? '</details>\n'
: '</div>\n';
}
return originalBlockquoteClose(tokens, idx, options, env, self);
};
};
18 changes: 18 additions & 0 deletions src/markdown-engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,15 @@ window["initRevealPresentation"] = async function() {
vscodePreviewPanel,
)}">`;

// style markdown-it-callout
styles += `<link rel="stylesheet" media="screen" href="${utility.addFileProtocol(
path.resolve(
utility.getCrossnoteBuildDirectory(),
'./styles/markdown-it-callout.css',
),
vscodePreviewPanel,
)}">`;

// global styles
styles += `<style>${this.notebook.config.globalCss}</style>`;

Expand Down Expand Up @@ -1772,6 +1781,15 @@ sidebarTOCBtn.addEventListener('click', function(event) {
),
)
: '',
// markdown-it-callout
outputHTML.indexOf('callout') > 0
? await this.fs.readFile(
path.resolve(
Comment on lines +1784 to +1787
utility.getCrossnoteBuildDirectory(),
Comment on lines +1784 to +1788
'./styles/markdown-it-callout.css',
),
)
: '',
]);
styleCSS = styles.join('');
} catch (e) {
Expand Down
2 changes: 2 additions & 0 deletions src/notebook/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Token from 'markdown-it/lib/token';
import * as path from 'path';
import { URI, Utils } from 'vscode-uri';
import useMarkdownAdmonition from '../custom-markdown-it-features/admonition';
import useMarkdownCallout from '../custom-markdown-it-features/callout';
import useMarkdownItCodeFences from '../custom-markdown-it-features/code-fences';
import useMarkdownItCriticMarkup from '../custom-markdown-it-features/critic-markup';
import useMarkdownItCurlyBracketAttributes from '../custom-markdown-it-features/curly-bracket-attributes';
Expand Down Expand Up @@ -158,6 +159,7 @@ export class Notebook {
useMarkdownItMath(md, this);
useMarkdownItWikilink(md, this);
useMarkdownAdmonition(md);
useMarkdownCallout(md);
useMarkdownItSourceMap(md);
useMarkdownItWidget(md, this);
return md;
Expand Down
157 changes: 157 additions & 0 deletions styles/markdown-it-callout.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: local("Material Icons"), local("MaterialIcons-Regular"), url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff");
Comment on lines +1 to +5
}


.callout {
--callout-color: 68, 138, 255;
position: relative;
overflow: hidden;
border-radius: 4px;
margin: 1rem 0;
padding: 0.75rem 1rem;
background: rgba(var(--callout-color), 0.1);
}

.callout>.callout-title {
margin: 0 -1.2rem;
padding: 0rem 1.2rem 0rem 3.3rem;
font-weight: 700;
color: rgb(var(--callout-color));
}

.callout .callout-title::before {
position: absolute;
left: 1.2rem;
font-size: 1.2rem;
font-family: Material Icons;
font-style: normal;
font-variant: normal;
font-weight: 400;
text-transform: none;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
color: rgba(var(--callout-color), 0.8);
content: var(--callout-icon, "i")
}

.callout>p {
margin-block-start: 1rem;
margin-block-end: 1rem;
padding-left: .4rem;
}

.callout details.callout-title,
.callout summary.callout-title {
cursor: pointer;
list-style: none;
}
Comment on lines +48 to +52

.callout summary.callout-title::-webkit-details-marker {
display: none;
}

details.callout > summary.callout-title::after {
display: inline-block;
width: 0.45em;
height: 0.45em;
margin-left: 1em;
border-right: 2px solid rgb(var(--callout-color));
border-bottom: 2px solid rgb(var(--callout-color));
transform: translateY(-0.05em) rotate(-45deg);
transition: transform 160ms ease, opacity 160ms ease;
opacity: 0.9;
content: "";
}

details.callout[open] > summary.callout-title::after {
transform: translateY(-0.05em) rotate(45deg);
opacity: 1;
}

.callout[data-callout="summary"],
.callout[data-callout="tldr"],
.callout[data-callout="abstract"] {
--callout-color: 0, 191, 188;
--callout-icon: "\E8D2";
}

.callout[data-callout="note"] {
--callout-color: 8, 109, 221;
--callout-icon: "\E3C9";
}

.callout[data-callout="info"] {
--callout-color: 8, 109, 221;
--callout-icon: "\E88E";
}

.callout[data-callout="todo"] {
--callout-color: 8, 109, 221;
--callout-icon: "\E88E";
}

.callout[data-callout="hint"],
.callout[data-callout="important"],
.callout[data-callout="tip"] {
--callout-color: 0, 191, 188;
--callout-icon: "\E80E";
}

.callout[data-callout="check"],
.callout[data-callout="done"],
.callout[data-callout="success"] {
--callout-color: 8, 185, 78;
--callout-icon: "\E876";
}

.callout[data-callout="help"],
.callout[data-callout="faq"],
.callout[data-callout="question"] {
--callout-color: 236, 117, 0;
--callout-icon: "\E887";
}

.callout[data-callout="caution"],
.callout[data-callout="attention"],
.callout[data-callout="warning"] {
--callout-color: 236, 117, 0;
--callout-icon: "\E002";
}

.callout[data-callout="fail"],
.callout[data-callout="missing"],
.callout[data-callout="failure"] {
--callout-color: 233, 49, 71;
--callout-icon: "\E14C";
}

.callout[data-callout="error"],
.callout[data-callout="danger"] {
--callout-color: 233, 49, 71;
--callout-icon: "\E3E7";
}

.callout[data-callout="bug"] {
--callout-color: 233, 49, 71;
--callout-icon: "\E868";
}

.callout[data-callout="example"] {
--callout-color: 120, 82, 238;
--callout-icon: "\E242";
}

.callout[data-callout="cite"],
.callout[data-callout="quote"] {
--callout-color: 158, 158, 158;
--callout-icon: "\E244";
}

.callout .callout {
margin: 0.75rem 0;
}
Loading