feat(markdown): add ??++
All checks were successful
/ deploy-and-package (push) Successful in 1m17s

This commit is contained in:
Alois 2026-08-09 23:41:51 +02:00
commit f50e7e9b71
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
4 changed files with 48 additions and 35 deletions

View file

@ -3,37 +3,3 @@
```sh ```sh
pnpm install https://git.methanium.net/methanium/ui/releases/download/latest/methanium-ui.tgz pnpm install https://git.methanium.net/methanium/ui/releases/download/latest/methanium-ui.tgz
``` ```
## Markdown
The renderer is available from the package root. The live editor, compact text
renderer, and emoji APIs use the `@methanium/ui/markdown` entry point.
```tsx
import { Markdown } from "@methanium/ui";
import {
EmojiProvider,
Input,
createEmojiRegistry,
} from "@methanium/ui/markdown";
const registry = createEmojiRegistry({
customEmojis: [
{
shortcode: ":party_parrot:",
aliases: [":parrot_party:"],
src: "/emoji/party-parrot.webp",
},
],
});
<EmojiProvider registry={registry}>
<Input value={value} setValue={setValue} />
<Markdown content={value} />
</EmojiProvider>;
```
Custom shortcodes and aliases must be unique and may not collide with built-in
Twemoji names. Twemoji graphics are bundled locally and do not use a CDN. The
graphics are provided by the [Twemoji project](https://github.com/jdecked/twemoji)
under CC-BY 4.0; code is licensed under MIT.

View file

@ -1,6 +1,6 @@
{ {
"name": "@methanium/ui", "name": "@methanium/ui",
"version": "0.0.25", "version": "0.0.26",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",

View file

@ -38,6 +38,7 @@ import { EmojiProvider, useEmojiRegistry } from "./emoji-context";
import type { EmojiRegistry } from "./emoji-data"; import type { EmojiRegistry } from "./emoji-data";
import { parseCodeMeta, remarkCodeMeta } from "./markdown-meta"; import { parseCodeMeta, remarkCodeMeta } from "./markdown-meta";
import { remarkEmoji } from "./remark-emoji"; import { remarkEmoji } from "./remark-emoji";
import { remarkSmallMuted } from "./remark-small-muted";
export type MarkdownTheme = "light" | "dark"; export type MarkdownTheme = "light" | "dark";
export type MarkdownVariant = "article" | "compact"; export type MarkdownVariant = "article" | "compact";
@ -147,6 +148,7 @@ function Markdown({
remarkMath, remarkMath,
remarkBreaks, remarkBreaks,
remarkCodeMeta, remarkCodeMeta,
remarkSmallMuted,
[remarkEmoji, { registry }], [remarkEmoji, { registry }],
]; ];

View file

@ -0,0 +1,45 @@
import type { Emphasis, Parent, Root, Text } from "mdast";
import { SKIP, visit } from "unist-util-visit";
const delimiter = "??++";
export function remarkSmallMuted() {
return (tree: Root) => {
visit(tree, "text", (node: Text, index, parent: Parent | undefined) => {
if (index === undefined || !parent) return;
const children: Parent["children"] = [];
let cursor = 0;
let opening = node.value.indexOf(delimiter);
while (opening !== -1) {
const contentStart = opening + delimiter.length;
const closing = node.value.indexOf(delimiter, contentStart);
if (closing === -1) break;
if (opening > cursor) {
children.push({ type: "text", value: node.value.slice(cursor, opening) });
}
children.push({
type: "emphasis",
children: [{ type: "text", value: node.value.slice(contentStart, closing) }],
data: {
hName: "span",
hProperties: { className: ["text-xs", "text-muted-foreground"] },
},
} satisfies Emphasis);
cursor = closing + delimiter.length;
opening = node.value.indexOf(delimiter, cursor);
}
if (children.length === 0) return;
if (cursor < node.value.length) {
children.push({ type: "text", value: node.value.slice(cursor) });
}
parent.children.splice(index, 1, ...children);
return [SKIP, index + children.length];
});
};
}