Compare commits
2 Commits
110451ae04
...
e4b980202e
| Author | SHA1 | Date |
|---|---|---|
|
|
e4b980202e | |
|
|
e861b80f57 |
|
|
@ -1 +1,12 @@
|
|||
:md-font[Pacifico]{source=google} hello?
|
||||
:md-dice[d6]{.bg-amber-100}
|
||||
|
||||
:md-border[crimson]{.b}
|
||||
|
||||
:::div{.flex.justify-stretch.gap-1}
|
||||
|
||||
foo
|
||||
|
||||
bar
|
||||
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ export const docEntries: DocEntry[] = [
|
|||
description: "点击文字执行掷骰,可用于属性检定、伤害投掷等场景。",
|
||||
syntax: ':md-dice[2d6+d8]{key="attack"}',
|
||||
props: [
|
||||
{ name: "key", type: "string", desc: "URL 参数标识,结果记录到 ?dice-key=15" },
|
||||
{
|
||||
name: "key",
|
||||
type: "string",
|
||||
desc: "URL 参数标识,结果记录到 ?dice-key=15",
|
||||
},
|
||||
],
|
||||
examples: `**攻击检定:** :md-dice[1d20+5]{key="attack"}
|
||||
|
||||
|
|
@ -32,8 +36,9 @@ export const docEntries: DocEntry[] = [
|
|||
tag: "md-table",
|
||||
icon: "📊",
|
||||
title: "表格组件",
|
||||
description: "将 CSV 数据转换为可切换标签页的表格,支持随机抽取和变量引用。",
|
||||
syntax: ':md-table[./data.csv]{roll=true remix=true}',
|
||||
description:
|
||||
"将 CSV 数据转换为可切换标签页的表格,支持随机抽取和变量引用。",
|
||||
syntax: ":md-table[./data.csv]{roll=true remix=true}",
|
||||
props: [
|
||||
{ name: "roll", type: "boolean", desc: "显示随机切换按钮" },
|
||||
{ name: "remix", type: "boolean", desc: "支持 {{prop}} 引用同行其他列" },
|
||||
|
|
@ -61,7 +66,7 @@ CSV 要求包含 label, body 列,可选 group 列分组。支持 YAML front ma
|
|||
icon: "🔗",
|
||||
title: "链接组件",
|
||||
description: "点击链接在当前页面内展开显示目标文章内容,支持章节定位。",
|
||||
syntax: ':md-link[./rules.md#combat]',
|
||||
syntax: ":md-link[./rules.md#combat]",
|
||||
props: [],
|
||||
examples: `**展开完整文档:**
|
||||
:md-link[./rules.md]
|
||||
|
|
@ -76,12 +81,28 @@ CSV 要求包含 label, body 列,可选 group 列分组。支持 YAML front ma
|
|||
tag: "md-pins",
|
||||
icon: "📍",
|
||||
title: "标记组件",
|
||||
description: "在地图或图片上添加可编辑或固定的位置标记,支持字母或数字标签。",
|
||||
description:
|
||||
"在地图或图片上添加可编辑或固定的位置标记,支持字母或数字标签。",
|
||||
syntax: ':md-pins[./images/map.png]{pins="A:30,40 B:10,30" fixed}',
|
||||
props: [
|
||||
{ name: "pins", type: "string", default: '""', desc: '标记列表,格式 "A:x,y B:x,y"' },
|
||||
{ name: "fixed", type: "boolean", default: "false", desc: "固定模式(只读不可编辑)" },
|
||||
{ name: "labelStart", type: "string", default: '"A"', desc: "标签起始值,支持字母或数字" },
|
||||
{
|
||||
name: "pins",
|
||||
type: "string",
|
||||
default: '""',
|
||||
desc: '标记列表,格式 "A:x,y B:x,y"',
|
||||
},
|
||||
{
|
||||
name: "fixed",
|
||||
type: "boolean",
|
||||
default: "false",
|
||||
desc: "固定模式(只读不可编辑)",
|
||||
},
|
||||
{
|
||||
name: "labelStart",
|
||||
type: "string",
|
||||
default: '"A"',
|
||||
desc: "标签起始值,支持字母或数字",
|
||||
},
|
||||
],
|
||||
examples: `**固定标记(只读):**
|
||||
:md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30" fixed}
|
||||
|
|
@ -102,7 +123,12 @@ CSV 要求包含 label, body 列,可选 group 列分组。支持 YAML front ma
|
|||
description: "将图片设置为当前文章卡片的背景,支持多种适配方式。",
|
||||
syntax: ':md-bg[./images/dungeon-bg.jpg]{fit="cover"}',
|
||||
props: [
|
||||
{ name: "fit", type: "cover | contain | fill | none | scale-down", default: "cover", desc: "背景适配方式" },
|
||||
{
|
||||
name: "fit",
|
||||
type: "cover | contain | fill | none | scale-down",
|
||||
default: "cover",
|
||||
desc: "背景适配方式",
|
||||
},
|
||||
],
|
||||
examples: `**设置背景图:**
|
||||
:md-bg[./images/dungeon-bg.jpg]{fit="cover"}
|
||||
|
|
@ -114,10 +140,16 @@ CSV 要求包含 label, body 列,可选 group 列分组。支持 YAML front ma
|
|||
tag: "md-font",
|
||||
icon: "🔤",
|
||||
title: "字体组件",
|
||||
description: "设置整个文档的字体,支持 Google Fonts、emfont 和系统本地字体三种来源。",
|
||||
description:
|
||||
"设置整个文档的字体,支持 Google Fonts、emfont 和系统本地字体三种来源。",
|
||||
syntax: ':md-font[Noto Sans SC]{source="google" weight="400"}',
|
||||
props: [
|
||||
{ name: "source", type: "google | emfont | local", default: "local", desc: "字体来源" },
|
||||
{
|
||||
name: "source",
|
||||
type: "google | emfont | local",
|
||||
default: "local",
|
||||
desc: "字体来源",
|
||||
},
|
||||
{ name: "weight", type: "string", default: '"400"', desc: "字体粗细" },
|
||||
],
|
||||
examples: `**Google 字体:**
|
||||
|
|
@ -132,12 +164,47 @@ CSV 要求包含 label, body 列,可选 group 列分组。支持 YAML front ma
|
|||
字体将应用到整个文档卡片。`,
|
||||
usage: "用于切换文档的显示字体,适配不同风格需求。",
|
||||
},
|
||||
{
|
||||
tag: "md-border",
|
||||
icon: "🖼️",
|
||||
title: "边框组件",
|
||||
description: "为文档卡片添加纯色或图片边框,支持指定边、样式和重复模式。",
|
||||
syntax: ":md-border[#3b82f6]{.l .dashed}",
|
||||
props: [
|
||||
{
|
||||
name: "width",
|
||||
type: "string",
|
||||
default: "纯色 .1mm,图片 2mm",
|
||||
desc: "边框宽度",
|
||||
},
|
||||
{
|
||||
name: "slice",
|
||||
type: "string",
|
||||
default: '"10"',
|
||||
desc: "border-image-slice(仅图片模式)",
|
||||
},
|
||||
],
|
||||
examples: `**纯色左边框:**
|
||||
:md-border[#3b82f6]{.l}
|
||||
|
||||
**虚线上下边框:**
|
||||
:md-border[red]{.t .b .dashed width=".2mm"}
|
||||
|
||||
**图片边框:**
|
||||
:md-border[./images/frame.png]{.all .round}
|
||||
|
||||
**左右图片竖条:**
|
||||
:md-border[./images/ornament.png]{.l .r .repeat}
|
||||
|
||||
边 class:\`t\` \`b\` \`l\` \`r\` \`all\`(默认)\n样式 class:\`solid\` \`dashed\` \`dotted\` \`double\`\n图片 class:\`stretch\` \`repeat\` \`round\` \`space\``,
|
||||
usage: "用于装饰文档卡片,配合背景和字体营造特定风格。",
|
||||
},
|
||||
{
|
||||
tag: "md-embed",
|
||||
icon: "📄",
|
||||
title: "嵌入组件",
|
||||
description: "将另一个 Markdown 文件的内容内联嵌入到当前文档中。",
|
||||
syntax: ':md-embed[./rules.md#combat]',
|
||||
syntax: ":md-embed[./rules.md#combat]",
|
||||
props: [],
|
||||
examples: `**嵌入完整文档:**
|
||||
:md-embed[./rules.md]
|
||||
|
|
@ -153,10 +220,15 @@ CSV 要求包含 label, body 列,可选 group 列分组。支持 YAML front ma
|
|||
icon: "🃏",
|
||||
title: "卡牌组件",
|
||||
description: "将 CSV 数据渲染为卡牌布局,支持自定义网格和图层的排版。",
|
||||
syntax: ':md-deck[./cards.csv]{grid="5x8" layers="title:1,1-5,1f8 body:1,5-8,8f3"}',
|
||||
syntax:
|
||||
':md-deck[./cards.csv]{grid="5x8" layers="title:1,1-5,1f8 body:1,5-8,8f3"}',
|
||||
props: [
|
||||
{ name: "grid", type: "string", desc: "卡牌布局,格式 行x列" },
|
||||
{ name: "layers", type: "string", desc: "图层定义,格式 字段:行,列-列,字号" },
|
||||
{
|
||||
name: "layers",
|
||||
type: "string",
|
||||
desc: "图层定义,格式 字段:行,列-列,字号",
|
||||
},
|
||||
],
|
||||
examples: `**基础卡牌:**
|
||||
:md-deck[./spells.csv]{grid="3x3"}
|
||||
|
|
@ -172,7 +244,7 @@ CSV 包含 label 和显示字段列,通过图层定义控制各字段的位置
|
|||
icon: "🧶",
|
||||
title: "叙事线组件",
|
||||
description: "展示 Yarn Spinner 格式的分支叙事结构,支持对话选择和分支。",
|
||||
syntax: ':md-yarn-spinner[./story.yarn]',
|
||||
syntax: ":md-yarn-spinner[./story.yarn]",
|
||||
props: [],
|
||||
examples: `**加载叙事文件:**
|
||||
:md-yarn-spinner[./story.yarn]
|
||||
|
|
@ -185,7 +257,7 @@ Yarn Spinner 是用于游戏对话系统的格式,支持选项、条件分支
|
|||
icon: "🪙",
|
||||
title: "代币组件",
|
||||
description: "展示游戏代币或棋子的图片。",
|
||||
syntax: ':md-token[./token.png]',
|
||||
syntax: ":md-token[./token.png]",
|
||||
props: [],
|
||||
examples: `**展示代币:**
|
||||
:md-token[./goblin.png]
|
||||
|
|
@ -197,8 +269,9 @@ Yarn Spinner 是用于游戏对话系统的格式,支持选项、条件分支
|
|||
tag: "md-token-viewer",
|
||||
icon: "🎨",
|
||||
title: "代币预览组件",
|
||||
description: "使用 Three.js 在浏览器中 3D 渲染 3MF 格式的代币模型,支持旋转查看。",
|
||||
syntax: ':md-token-viewer[./token.3mf]',
|
||||
description:
|
||||
"使用 Three.js 在浏览器中 3D 渲染 3MF 格式的代币模型,支持旋转查看。",
|
||||
syntax: ":md-token-viewer[./token.3mf]",
|
||||
props: [],
|
||||
examples: `**3D 预览模型:**
|
||||
:md-token-viewer[./dragon.3mf]
|
||||
|
|
@ -211,7 +284,7 @@ Yarn Spinner 是用于游戏对话系统的格式,支持选项、条件分支
|
|||
icon: "📋",
|
||||
title: "命令追踪器",
|
||||
description: "支持命令历史和游戏状态追踪,使用类 Emmet 语法创建追踪项。",
|
||||
syntax: ':md-commander',
|
||||
syntax: ":md-commander",
|
||||
props: [],
|
||||
examples: `**追踪 NPC 血量和防御:**
|
||||
\`\`\`
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import "./md-table";
|
|||
import "./md-link";
|
||||
import "./md-pins";
|
||||
import "./md-bg";
|
||||
import "./md-border";
|
||||
import "./md-font";
|
||||
import "./md-embed";
|
||||
import "./md-deck";
|
||||
|
|
@ -26,6 +27,7 @@ export type { DiceProps } from "./md-dice";
|
|||
export type { TableProps } from "./md-table";
|
||||
export type { BgProps } from "./md-bg";
|
||||
export type { FontProps } from "./md-font";
|
||||
export type { BorderProps } from "./md-border";
|
||||
export type { EmbedProps } from "./md-embed";
|
||||
export type { YarnSpinnerProps } from "./md-yarn-spinner";
|
||||
export type { TokenProps } from "./md-token";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
import { customElement, noShadowDOM } from "solid-element";
|
||||
import { onCleanup } from "solid-js";
|
||||
import { resolvePath } from "./utils/path";
|
||||
import { registerStyle } from "./utils/article-style-manager";
|
||||
|
||||
const SIDE_CLASSES = new Set(["t", "b", "l", "r", "all"]);
|
||||
const STYLE_CLASSES = new Set([
|
||||
"solid",
|
||||
"dashed",
|
||||
"dotted",
|
||||
"double",
|
||||
"groove",
|
||||
"ridge",
|
||||
"inset",
|
||||
"outset",
|
||||
"none",
|
||||
]);
|
||||
const REPEAT_CLASSES = new Set(["stretch", "repeat", "round", "space"]);
|
||||
|
||||
const SIDE_CSS: Record<string, string> = {
|
||||
t: "Top",
|
||||
b: "Bottom",
|
||||
l: "Left",
|
||||
r: "Right",
|
||||
};
|
||||
|
||||
function parseSides(classes: string[]): string[] {
|
||||
const sides = classes.filter((c) => SIDE_CLASSES.has(c));
|
||||
if (sides.length === 0) return ["Top", "Bottom", "Left", "Right"];
|
||||
if (sides.includes("all")) return ["Top", "Bottom", "Left", "Right"];
|
||||
return sides.map((s) => SIDE_CSS[s]).filter(Boolean);
|
||||
}
|
||||
|
||||
function parseStyle(classes: string[]): string {
|
||||
const s = classes.find((c) => STYLE_CLASSES.has(c));
|
||||
return s || "solid";
|
||||
}
|
||||
|
||||
function parseRepeat(classes: string[]): string {
|
||||
const r = classes.find((c) => REPEAT_CLASSES.has(c));
|
||||
return r || "stretch";
|
||||
}
|
||||
|
||||
function isImagePath(value: string): boolean {
|
||||
return /^(\.{0,2}\/|[a-zA-Z]:\\|https?:\/\/)|\.(png|jpg|jpeg|gif|svg|webp|bmp)(\?|$)/i.test(
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
export interface BorderProps {
|
||||
width?: string;
|
||||
slice?: string;
|
||||
}
|
||||
|
||||
customElement(
|
||||
"md-border",
|
||||
{ width: undefined, slice: "10" },
|
||||
(props, { element }) => {
|
||||
noShadowDOM();
|
||||
|
||||
const rawValue = element?.textContent?.trim() || "";
|
||||
if (element) element.textContent = "";
|
||||
|
||||
const articleEl = element?.closest("article") as HTMLElement;
|
||||
const articlePath =
|
||||
element?.closest("article[data-src]")?.getAttribute("data-src") || "";
|
||||
|
||||
// Parse classes from the element's className (marked-directive passes
|
||||
// attrs.class as the element's className)
|
||||
const classList = element?.className ? element.className.split(/\s+/) : [];
|
||||
|
||||
const sides = parseSides(classList);
|
||||
const image = isImagePath(rawValue);
|
||||
const resolvedSrc = image ? resolvePath(articlePath, rawValue) : "";
|
||||
|
||||
const styles: Partial<CSSStyleDeclaration> = {};
|
||||
|
||||
if (image) {
|
||||
const repeat = parseRepeat(classList);
|
||||
const borderWidth = props.width || "2mm";
|
||||
styles.borderImageSource = `url(${resolvedSrc})`;
|
||||
styles.borderImageSlice = props.slice || "10";
|
||||
styles.borderImageWidth = borderWidth;
|
||||
styles.borderImageRepeat = repeat;
|
||||
styles.borderImageOutset = "0";
|
||||
(styles as any).borderStyle = "solid";
|
||||
|
||||
const allSides = ["Top", "Bottom", "Left", "Right"];
|
||||
for (const side of allSides) {
|
||||
if (sides.includes(side)) {
|
||||
(styles as any)[`border${side}Width`] = borderWidth;
|
||||
} else {
|
||||
(styles as any)[`border${side}Width`] = "0";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const color = rawValue || "currentColor";
|
||||
const style = parseStyle(classList);
|
||||
const borderWidth = props.width || ".2mm";
|
||||
|
||||
for (const side of sides) {
|
||||
(styles as any)[`border${side}Width`] = borderWidth;
|
||||
(styles as any)[`border${side}Style`] = style;
|
||||
(styles as any)[`border${side}Color`] = color;
|
||||
}
|
||||
}
|
||||
|
||||
const handle = registerStyle({
|
||||
key: "border",
|
||||
article: articleEl,
|
||||
styles,
|
||||
});
|
||||
|
||||
onCleanup(() => handle.dispose());
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
Loading…
Reference in New Issue