mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-10-18 19:10:45 +08:00
Render GitHub style alerts
A blockquote starting with `[!NOTE]` or one of its siblings showed up as a plain quote, while GitHub and the web explorer render it as a titled callout. Co-authored-by: Rūdolfs Ošiņš <rudolfs@osins.org>
This commit is contained in:
committed by
Rūdolfs Ošiņš
co-authored by
Rūdolfs Ošiņš
parent
1bc5ffa9fe
commit
70046d0c74
@@ -43,6 +43,8 @@
|
|||||||
--color-feedback-error-border: var(--color-semantic-red-600);
|
--color-feedback-error-border: var(--color-semantic-red-600);
|
||||||
--color-feedback-error-bg: var(--color-semantic-red-100);
|
--color-feedback-error-bg: var(--color-semantic-red-100);
|
||||||
--color-feedback-error-bg-selected: var(--color-semantic-red-200);
|
--color-feedback-error-bg-selected: var(--color-semantic-red-200);
|
||||||
|
--color-feedback-info-text: var(--color-semantic-blue-800);
|
||||||
|
--color-feedback-important-text: var(--color-accent-purple-800);
|
||||||
|
|
||||||
--color-diff-addition: var(--color-semantic-green-500);
|
--color-diff-addition: var(--color-semantic-green-500);
|
||||||
--color-diff-deletion: var(--color-semantic-red-500);
|
--color-diff-deletion: var(--color-semantic-red-500);
|
||||||
@@ -121,6 +123,8 @@
|
|||||||
--color-feedback-error-border: var(--color-semantic-red-700);
|
--color-feedback-error-border: var(--color-semantic-red-700);
|
||||||
--color-feedback-error-bg: var(--color-semantic-red-900);
|
--color-feedback-error-bg: var(--color-semantic-red-900);
|
||||||
--color-feedback-error-bg-selected: var(--color-semantic-red-800);
|
--color-feedback-error-bg-selected: var(--color-semantic-red-800);
|
||||||
|
--color-feedback-info-text: var(--color-semantic-blue-400);
|
||||||
|
--color-feedback-important-text: var(--color-accent-purple-400);
|
||||||
|
|
||||||
--color-diff-addition: var(--color-semantic-green-400);
|
--color-diff-addition: var(--color-semantic-green-400);
|
||||||
--color-diff-deletion: var(--color-semantic-red-400);
|
--color-diff-deletion: var(--color-semantic-red-400);
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
const alertIcons = {
|
||||||
|
note: "guide",
|
||||||
|
tip: "lightbulb",
|
||||||
|
important: "comment",
|
||||||
|
warning: "warning",
|
||||||
|
caution: "stop",
|
||||||
|
} as const;
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { mount, tick, unmount } from "svelte";
|
import { mount, tick, unmount } from "svelte";
|
||||||
|
|
||||||
@@ -6,6 +16,7 @@
|
|||||||
import { parseFrontmatter } from "@app/lib/frontmatter";
|
import { parseFrontmatter } from "@app/lib/frontmatter";
|
||||||
import { invoke } from "@app/lib/invoke";
|
import { invoke } from "@app/lib/invoke";
|
||||||
import {
|
import {
|
||||||
|
alertVariants,
|
||||||
isTaskCheckbox,
|
isTaskCheckbox,
|
||||||
maximumReferences,
|
maximumReferences,
|
||||||
renderMarkdown,
|
renderMarkdown,
|
||||||
@@ -332,6 +343,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const title of container.querySelectorAll("p.alert-title")) {
|
||||||
|
const variant = alertVariants.find(name =>
|
||||||
|
title.parentElement?.classList.contains(`alert-${name}`),
|
||||||
|
);
|
||||||
|
if (!variant) continue;
|
||||||
|
|
||||||
|
icons.push(
|
||||||
|
mount(Icon, {
|
||||||
|
target: title,
|
||||||
|
anchor: title.firstChild ?? undefined,
|
||||||
|
props: { name: alertIcons[variant] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
enhanceCodeBlocks(container);
|
enhanceCodeBlocks(container);
|
||||||
|
|
||||||
if (!scrolledToHash && window.location.hash) {
|
if (!scrolledToHash && window.location.hash) {
|
||||||
@@ -657,6 +683,38 @@
|
|||||||
.markdown :global(dl dd) {
|
.markdown :global(dl dd) {
|
||||||
margin: 0 0 0 2rem;
|
margin: 0 0 0 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown :global(.alert) {
|
||||||
|
border-left: 0.3rem solid var(--alert-color);
|
||||||
|
padding: 0 0 0 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.markdown :global(.alert > .alert-title) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--alert-color);
|
||||||
|
font: var(--txt-body-m-semibold);
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
}
|
||||||
|
.markdown :global(.alert > :last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.markdown :global(.alert-note) {
|
||||||
|
--alert-color: var(--color-feedback-info-text);
|
||||||
|
}
|
||||||
|
.markdown :global(.alert-tip) {
|
||||||
|
--alert-color: var(--color-feedback-success-text);
|
||||||
|
}
|
||||||
|
.markdown :global(.alert-important) {
|
||||||
|
--alert-color: var(--color-feedback-important-text);
|
||||||
|
}
|
||||||
|
.markdown :global(.alert-warning) {
|
||||||
|
--alert-color: var(--color-feedback-warning-text);
|
||||||
|
}
|
||||||
|
.markdown :global(.alert-caution) {
|
||||||
|
--alert-color: var(--color-feedback-error-text);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{#if frontMatter && frontMatter.length > 0}
|
{#if frontMatter && frontMatter.length > 0}
|
||||||
|
|||||||
+57
-1
@@ -98,6 +98,58 @@ const anchorMarkedExtension = {
|
|||||||
renderer: (token: Tokens.Generic): string => `<a name="${token.text}"></a>`,
|
renderer: (token: Tokens.Generic): string => `<a name="${token.text}"></a>`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// GitHub flavoured alerts: a blockquote whose first line is a `[!NOTE]` marker
|
||||||
|
// renders as a titled callout rather than a quote.
|
||||||
|
export const alertVariants = [
|
||||||
|
"note",
|
||||||
|
"tip",
|
||||||
|
"important",
|
||||||
|
"warning",
|
||||||
|
"caution",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type AlertVariant = (typeof alertVariants)[number];
|
||||||
|
|
||||||
|
function isAlertVariant(name: string): name is AlertVariant {
|
||||||
|
return (alertVariants as readonly string[]).includes(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertMarkedExtension: TokenizerExtension & RendererExtension = {
|
||||||
|
name: "alert",
|
||||||
|
level: "block",
|
||||||
|
start: (src: string) => src.match(/^ {0,3}> *\[!/m)?.index,
|
||||||
|
tokenizer(src: string) {
|
||||||
|
const quote = /^(?: {0,3}>[^\n]*(?:\n|$))+/.exec(src);
|
||||||
|
if (!quote) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = quote[0].replace(/^ {0,3}> ?/gm, "");
|
||||||
|
const marker = /^\[!([a-zA-Z]+)\][^\S\n]*(?:\n|$)/.exec(body);
|
||||||
|
if (!marker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variant = marker[1].toLowerCase();
|
||||||
|
if (!isAlertVariant(variant)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "alert",
|
||||||
|
raw: quote[0],
|
||||||
|
variant,
|
||||||
|
tokens: this.lexer.blockTokens(body.slice(marker[0].length)),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
renderer(token: Tokens.Generic): string {
|
||||||
|
const variant = token.variant as AlertVariant;
|
||||||
|
const title = variant.charAt(0).toUpperCase() + variant.slice(1);
|
||||||
|
|
||||||
|
return `<div class="alert alert-${variant}"><p class="alert-title">${title}</p>${this.parser.parse(token.tokens ?? [])}</div>`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Past this many references in one document the rest stay text, matching
|
// Past this many references in one document the rest stay text, matching
|
||||||
// the number of chips `Markdown` mounts.
|
// the number of chips `Markdown` mounts.
|
||||||
export const maximumReferences = 200;
|
export const maximumReferences = 200;
|
||||||
@@ -180,7 +232,11 @@ export const markdownWithExtensions = new Marked(
|
|||||||
markedFootnote({ refMarkers: true }),
|
markedFootnote({ refMarkers: true }),
|
||||||
markedEmoji({ emojis }),
|
markedEmoji({ emojis }),
|
||||||
((): MarkedExtension => ({
|
((): MarkedExtension => ({
|
||||||
extensions: [anchorMarkedExtension, radicleReferenceMarkedExtension],
|
extensions: [
|
||||||
|
anchorMarkedExtension,
|
||||||
|
alertMarkedExtension,
|
||||||
|
radicleReferenceMarkedExtension,
|
||||||
|
],
|
||||||
}))(),
|
}))(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { RadiclePeer } from "@tests/support/peerManager.js";
|
||||||
|
|
||||||
|
import * as Fs from "node:fs/promises";
|
||||||
|
import * as Path from "node:path";
|
||||||
|
|
||||||
|
import { expect, test } from "@tests/support/fixtures.js";
|
||||||
|
import { createRepo } from "@tests/support/repo.js";
|
||||||
|
|
||||||
|
async function createRichRepo(peer: RadiclePeer) {
|
||||||
|
const { rid, repoFolder } = await createRepo(peer, { name: "rich" });
|
||||||
|
await Fs.writeFile(
|
||||||
|
Path.join(repoFolder, "README.md"),
|
||||||
|
["# Rich", "", "> [!NOTE]", "> Read the guide first.", ""].join("\n"),
|
||||||
|
);
|
||||||
|
await peer.git(["add", "."], { cwd: repoFolder });
|
||||||
|
await peer.git(["commit", "-m", "Write a rich readme"], { cwd: repoFolder });
|
||||||
|
await peer.git(["push", "rad", "main"], { cwd: repoFolder });
|
||||||
|
return { rid };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("render alerts in a readme", async ({ page, peer }) => {
|
||||||
|
const { rid } = await createRichRepo(peer);
|
||||||
|
await page.goto(`/repos/${rid}/home`);
|
||||||
|
|
||||||
|
const readme = page.locator(".markdown");
|
||||||
|
await expect(
|
||||||
|
readme.locator(".alert-note").getByText("Read the guide first."),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user