Compare commits
No commits in common. "f9d0620d3e62f3c07bacb508635760dd2af6f96e" and "3d9617f06c026203425829c96e75f9b8c36ef130" have entirely different histories.
f9d0620d3e
...
3d9617f06c
|
|
@ -145,33 +145,22 @@
|
||||||
```markdown
|
```markdown
|
||||||
:md-pins[./images/map.png]{pins="A:30,40 B:10,30" fixed}
|
:md-pins[./images/map.png]{pins="A:30,40 B:10,30" fixed}
|
||||||
:md-pins[./images/city-map.png]
|
:md-pins[./images/city-map.png]
|
||||||
:md-pins[./images/dungeon.png]{labelStart="1"}
|
|
||||||
:md-pins[./images/quest.png]{labelStart="C"}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**功能:**
|
**功能:**
|
||||||
- 在图片上显示标记点(A-Z, AA-ZZ, AAA-ZZZ... 或数字)
|
- 在图片上显示标记点(A-Z, AA-ZZ, AAA-ZZZ...)
|
||||||
- `fixed` 模式:仅显示标记,不可编辑
|
- `fixed` 模式:仅显示标记,不可编辑
|
||||||
- 非 fixed 模式:点击图片添加标记,点击标记删除
|
- 非 fixed 模式:点击图片添加标记,点击标记删除
|
||||||
- 提供复制按钮生成标记代码
|
- 提供复制按钮生成标记代码
|
||||||
- 支持自定义标签起始值(字母或数字)
|
|
||||||
|
|
||||||
**属性:**
|
**属性:**
|
||||||
|
|
||||||
| 属性 | 类型 | 默认值 | 说明 |
|
| 属性 | 类型 | 说明 |
|
||||||
|------|------|--------|------|
|
|
||||||
| `pins` | string | `""` | 标记列表,格式 `"A:30,40 B:10,30"` (标签:x,y) |
|
|
||||||
| `fixed` | boolean | `false` | 是否为固定模式(只读) |
|
|
||||||
| `labelStart` | string | `"A"` | 标签起始值,支持字母(如 `"A"`, `"C"`)或数字(如 `"1"`, `"7"`) |
|
|
||||||
|
|
||||||
**标签生成规则:**
|
|
||||||
|
|
||||||
| 起始值类型 | 说明 | 示例 |
|
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 字母(默认) | 从起始字母开始,按 A-Z, AA-ZZ, AAA-ZZZ 顺序 | `A` → A, B, C...; `C` → C, D, E... |
|
| `pins` | string | 标记列表,格式 `"A:30,40 B:10,30"` (标签:x,y) |
|
||||||
| 数字 | 从起始数字开始递增 | `1` → 1, 2, 3...; `7` → 7, 8, 9... |
|
| `fixed` | boolean | 是否为固定模式(只读) |
|
||||||
|
|
||||||
**字母标签序列(从 A 开始):**
|
**标记标签生成规则:**
|
||||||
- 0-25: A-Z
|
- 0-25: A-Z
|
||||||
- 26-51: AA-ZZ
|
- 26-51: AA-ZZ
|
||||||
- 52-77: AAA-ZZZ
|
- 52-77: AAA-ZZZ
|
||||||
|
|
@ -179,24 +168,12 @@
|
||||||
**示例:**
|
**示例:**
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
<!-- 固定标记模式 - 字母标签(默认从 A 开始) -->
|
<!-- 固定标记模式 -->
|
||||||
:md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30 C:50,80" fixed}
|
:md-pins[./images/battle-map.png]{pins="A:25,50 B:75,30 C:50,80" fixed}
|
||||||
|
|
||||||
<!-- 可编辑模式 - 点击添加标记 - 从 A 开始 -->
|
<!-- 可编辑模式 - 点击添加标记 -->
|
||||||
:md-pins[./images/blank-map.png]
|
:md-pins[./images/blank-map.png]
|
||||||
|
|
||||||
<!-- 数字标签 - 从 1 开始 -->
|
|
||||||
:md-pins[./images/dungeon.png]{labelStart="1"}
|
|
||||||
|
|
||||||
<!-- 数字标签 - 从 7 开始 -->
|
|
||||||
:md-pins[./images/cave.png]{labelStart="7"}
|
|
||||||
|
|
||||||
<!-- 字母标签 - 从 C 开始 -->
|
|
||||||
:md-pins[./images/quest-map.png]{labelStart="C"}
|
|
||||||
|
|
||||||
<!-- 自定义标签 + 预设标记 -->
|
|
||||||
:md-pins[./images/map.png]{pins="5:30,40 6:75,30" labelStart="5" fixed}
|
|
||||||
|
|
||||||
<!-- 复制生成的格式 -->
|
<!-- 复制生成的格式 -->
|
||||||
:md-pins[./images/map.png]{pins="A:30,40 B:10,30" fixed}
|
:md-pins[./images/map.png]{pins="A:30,40 B:10,30" fixed}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@ import {Show, For, createResource, createMemo, createSignal} from "solid-js";
|
||||||
import { resolvePath } from "./utils/path";
|
import { resolvePath } from "./utils/path";
|
||||||
import { createPinsStore } from "./stores/pinsStore";
|
import { createPinsStore } from "./stores/pinsStore";
|
||||||
|
|
||||||
customElement("md-pins", { pins: "", fixed: false, labelStart: "A" }, (props, { element }) => {
|
customElement("md-pins", { pins: "", fixed: false }, (props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
|
|
||||||
const [showToast, setShowToast] = createSignal(false);
|
const [showToast, setShowToast] = createSignal(false);
|
||||||
|
|
||||||
// 创建 store
|
// 创建 store
|
||||||
const store = createPinsStore(props.pins, props.fixed, props.labelStart);
|
const store = createPinsStore(props.pins, props.fixed);
|
||||||
|
|
||||||
// 从 element 的 textContent 获取图片路径
|
// 从 element 的 textContent 获取图片路径
|
||||||
const rawSrc = element?.textContent?.trim() || '';
|
const rawSrc = element?.textContent?.trim() || '';
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { customElement, noShadowDOM } from 'solid-element';
|
import { customElement, noShadowDOM } from 'solid-element';
|
||||||
import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js';
|
import { createSignal, For, Show, createEffect, createMemo, createResource } from 'solid-js';
|
||||||
import { marked } from '../markdown';
|
import { marked } from '../markdown';
|
||||||
import { loadCSV, CSV, processVariables, isCSV } from './utils/csv-loader';
|
|
||||||
import { resolvePath } from './utils/path';
|
import { resolvePath } from './utils/path';
|
||||||
import { areAllLabelsNumeric, weightedRandomIndex } from './utils/weighted-random';
|
import {loadCSV, CSV, processVariables} from './utils/csv-loader';
|
||||||
|
|
||||||
export interface TableProps {
|
export interface TableProps {
|
||||||
roll?: boolean;
|
roll?: boolean;
|
||||||
|
|
@ -24,8 +23,8 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
|
||||||
const [bodyHtml, setBodyHtml] = createSignal('');
|
const [bodyHtml, setBodyHtml] = createSignal('');
|
||||||
let tabsContainer: HTMLDivElement | undefined;
|
let tabsContainer: HTMLDivElement | undefined;
|
||||||
|
|
||||||
// 从 element 的 textContent 获取 CSV 路径或 inline CSV 数据
|
// 从 element 的 textContent 获取 CSV 路径
|
||||||
const rawContent = element?.textContent?.trim() || '';
|
const src = element?.textContent?.trim() || '';
|
||||||
|
|
||||||
// 隐藏原始文本内容
|
// 隐藏原始文本内容
|
||||||
if (element) {
|
if (element) {
|
||||||
|
|
@ -36,11 +35,11 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
|
||||||
const articleEl = element?.closest('article[data-src]');
|
const articleEl = element?.closest('article[data-src]');
|
||||||
const articlePath = articleEl?.getAttribute('data-src') || '';
|
const articlePath = articleEl?.getAttribute('data-src') || '';
|
||||||
|
|
||||||
// 如果是 inline CSV,直接使用;否则解析相对路径
|
// 解析相对路径
|
||||||
const contentOrPath = isCSV(rawContent) ? rawContent : resolvePath(articlePath, rawContent);
|
const resolvedSrc = resolvePath(articlePath, src);
|
||||||
|
|
||||||
// 使用 createResource 加载 CSV,自动响应路径变化并避免重复加载
|
// 使用 createResource 加载 CSV,自动响应路径变化并避免重复加载
|
||||||
const [csvData] = createResource(() => contentOrPath, loadCSV);
|
const [csvData] = createResource(() => resolvedSrc, loadCSV);
|
||||||
|
|
||||||
// 当数据加载完成后更新 rows
|
// 当数据加载完成后更新 rows
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|
@ -109,23 +108,8 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
|
||||||
|
|
||||||
// 随机切换 tab
|
// 随机切换 tab
|
||||||
const handleRoll = () => {
|
const handleRoll = () => {
|
||||||
const filtered = filteredRows();
|
const randomIndex = Math.floor(Math.random() * filteredRows().length);
|
||||||
if (filtered.length === 0) return;
|
|
||||||
|
|
||||||
// 检查所有 label 是否都是整数/整数范围格式
|
|
||||||
const labels = filtered.map(row => row.label);
|
|
||||||
if (areAllLabelsNumeric(labels)) {
|
|
||||||
// 使用加权随机
|
|
||||||
const randomIndex = weightedRandomIndex(labels);
|
|
||||||
if (randomIndex !== -1) {
|
|
||||||
setActiveTab(randomIndex);
|
setActiveTab(randomIndex);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 使用简单随机
|
|
||||||
const randomIndex = Math.floor(Math.random() * filtered.length);
|
|
||||||
setActiveTab(randomIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 滚动到可视区域
|
// 滚动到可视区域
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const activeButton = tabsContainer?.querySelector('.border-blue-600');
|
const activeButton = tabsContainer?.querySelector('.border-blue-600');
|
||||||
|
|
|
||||||
|
|
@ -10,31 +10,10 @@ interface PinsState {
|
||||||
pins: Pin[];
|
pins: Pin[];
|
||||||
rawSrc: string;
|
rawSrc: string;
|
||||||
isFixed: boolean;
|
isFixed: boolean;
|
||||||
labelStart: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断 labelStart 是否为数字
|
// 生成标签 A-Z, AA-ZZ, AAA-ZZZ ...
|
||||||
function isNumericLabelStart(start: string): boolean {
|
export function generateLabel(index: number): string {
|
||||||
return /^\d+$/.test(start);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成字母标签 A-Z, AA-ZZ, AAA-ZZZ ... 从指定起始值开始
|
|
||||||
export function generateAlphaLabel(index: number, startLabel: string = 'A'): string {
|
|
||||||
const startOffset = alphaLabelToIndex(startLabel);
|
|
||||||
return indexToAlphaLabel(index + startOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将字母标签转换为索引 (A=0, B=1, ..., Z=25, AA=26, ...)
|
|
||||||
export function alphaLabelToIndex(label: string): number {
|
|
||||||
let index = 0;
|
|
||||||
for (let i = 0; i < label.length; i++) {
|
|
||||||
index = index * 26 + (label.charCodeAt(i) - 64);
|
|
||||||
}
|
|
||||||
return index - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将索引转换为字母标签 (0=A, 1=B, ..., 25=Z, 26=AA, ...)
|
|
||||||
export function indexToAlphaLabel(index: number): string {
|
|
||||||
const labels: string[] = [];
|
const labels: string[] = [];
|
||||||
let num = index;
|
let num = index;
|
||||||
|
|
||||||
|
|
@ -47,18 +26,12 @@ export function indexToAlphaLabel(index: number): string {
|
||||||
return labels.join('');
|
return labels.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成数字标签,从指定起始值开始
|
// 解析 pins 字符串 "A:30,40 B:10,30" -> Pin[]
|
||||||
export function generateNumberLabel(index: number, startNum: number): string {
|
|
||||||
return String(startNum + index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析 pins 字符串 "A:30,40 B:10,30" 或 "1:30,40 2:10,30" -> Pin[]
|
|
||||||
export function parsePins(pinsStr: string): Pin[] {
|
export function parsePins(pinsStr: string): Pin[] {
|
||||||
if (!pinsStr) return [];
|
if (!pinsStr) return [];
|
||||||
|
|
||||||
const pins: Pin[] = [];
|
const pins: Pin[] = [];
|
||||||
// 支持任意非空白字符作为标签(不仅限于大写字母)
|
const regex = /([A-Z]+):(\d+),(\d+)/g;
|
||||||
const regex = /([^:\s]+):(\d+),(\d+)/g;
|
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
while ((match = regex.exec(pinsStr)) !== null) {
|
while ((match = regex.exec(pinsStr)) !== null) {
|
||||||
|
|
@ -78,52 +51,28 @@ export function formatPins(pins: Pin[]): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到最早未使用的标签
|
// 找到最早未使用的标签
|
||||||
export function findNextUnusedLabel(pins: Pin[], labelStart: string): string {
|
export function findNextUnusedLabel(pins: Pin[]): string {
|
||||||
const usedLabels = new Set(pins.map(p => p.label));
|
const usedLabels = new Set(pins.map(p => p.label));
|
||||||
|
|
||||||
if (isNumericLabelStart(labelStart)) {
|
|
||||||
// 数字标签模式
|
|
||||||
const startNum = parseInt(labelStart);
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
const label = generateNumberLabel(index, startNum);
|
const label = generateLabel(index);
|
||||||
if (!usedLabels.has(label)) {
|
if (!usedLabels.has(label)) {
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
if (index > 10000) break; // 安全限制
|
if (index > 10000) break; // 安全限制
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 字母标签模式
|
|
||||||
let index = 0;
|
|
||||||
while (true) {
|
|
||||||
const label = generateAlphaLabel(index, labelStart);
|
|
||||||
if (!usedLabels.has(label)) {
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
if (index > 10000) break; // 安全限制
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 回退值
|
return generateLabel(pins.length);
|
||||||
if (isNumericLabelStart(labelStart)) {
|
|
||||||
return generateNumberLabel(pins.length, parseInt(labelStart));
|
|
||||||
}
|
|
||||||
return generateAlphaLabel(pins.length, labelStart);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 store 实例
|
// 创建 store 实例
|
||||||
export function createPinsStore(
|
export function createPinsStore(initialPinsStr: string = "", initialFixed: boolean = false) {
|
||||||
initialPinsStr: string = "",
|
|
||||||
initialFixed: boolean = false,
|
|
||||||
initialLabelStart: string = "A"
|
|
||||||
) {
|
|
||||||
const [state, setState] = createStore<PinsState>({
|
const [state, setState] = createStore<PinsState>({
|
||||||
pins: parsePins(initialPinsStr),
|
pins: parsePins(initialPinsStr),
|
||||||
rawSrc: "",
|
rawSrc: "",
|
||||||
isFixed: initialFixed,
|
isFixed: initialFixed
|
||||||
labelStart: initialLabelStart
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const setRawSrc = (src: string) => {
|
const setRawSrc = (src: string) => {
|
||||||
|
|
@ -131,7 +80,7 @@ export function createPinsStore(
|
||||||
};
|
};
|
||||||
|
|
||||||
const addPin = (x: number, y: number) => {
|
const addPin = (x: number, y: number) => {
|
||||||
const label = findNextUnusedLabel(state.pins, state.labelStart);
|
const label = findNextUnusedLabel(state.pins);
|
||||||
setState("pins", [...state.pins, { x, y, label }]);
|
setState("pins", [...state.pins, { x, y, label }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -143,8 +92,7 @@ export function createPinsStore(
|
||||||
|
|
||||||
const getCopyText = () => {
|
const getCopyText = () => {
|
||||||
const pinsStr = formatCurrentPins();
|
const pinsStr = formatCurrentPins();
|
||||||
const labelStartAttr = state.labelStart !== 'A' ? ` labelStart="${state.labelStart}"` : '';
|
return `:md-pins[${state.rawSrc}]{pins="${pinsStr}" fixed}`;
|
||||||
return `:md-pins[${state.rawSrc}]{pins="${pinsStr}" fixed${labelStartAttr}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -31,87 +31,13 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检测字符串是否是 CSV 格式
|
|
||||||
* @param str 待检测的字符串
|
|
||||||
* @returns 如果是 CSV 格式返回 true
|
|
||||||
*/
|
|
||||||
export function isCSV(str: string): boolean {
|
|
||||||
const trimmed = str.trim();
|
|
||||||
|
|
||||||
// 检查是否以 YAML front matter 开头
|
|
||||||
if (trimmed.startsWith('---\n') || trimmed.startsWith('---\r\n')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否包含 CSV 特征:多行且有分隔符
|
|
||||||
const lines = trimmed.split(/\r?\n/).filter(line => line.trim() !== '');
|
|
||||||
if (lines.length < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检测常见 CSV 分隔符
|
|
||||||
const separators = [',', '\t', ';', '|'];
|
|
||||||
const firstLine = lines[0];
|
|
||||||
|
|
||||||
for (const sep of separators) {
|
|
||||||
if (firstLine.includes(sep)) {
|
|
||||||
// 检查其他行是否也有相同的分隔符
|
|
||||||
const hasSeparatorInOtherLines = lines.slice(1).some(line => line.includes(sep));
|
|
||||||
if (hasSeparatorInOtherLines) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析 CSV 字符串
|
|
||||||
* @template T 返回数据的类型,默认为 Record<string, string>
|
|
||||||
* @param csvString CSV 字符串内容
|
|
||||||
* @param sourcePath 源路径标识(用于标记数据来源)
|
|
||||||
* @returns 解析后的 CSV 数据
|
|
||||||
*/
|
|
||||||
export function parseCSVString<T = Record<string, string>>(csvString: string, sourcePath: string = 'inline'): CSV<T> {
|
|
||||||
// 解析 front matter
|
|
||||||
const { frontmatter, remainingContent } = parseFrontMatter(csvString);
|
|
||||||
|
|
||||||
const records = parse(remainingContent, {
|
|
||||||
columns: true,
|
|
||||||
comment: '#',
|
|
||||||
trim: true,
|
|
||||||
skipEmptyLines: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = records as Record<string, string>[];
|
|
||||||
// 添加 front matter 到结果中
|
|
||||||
const csvResult = result as CSV<T>;
|
|
||||||
if (frontmatter) {
|
|
||||||
csvResult.frontmatter = frontmatter;
|
|
||||||
for(const each of result){
|
|
||||||
Object.assign(each, frontmatter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
csvResult.sourcePath = sourcePath;
|
|
||||||
return csvResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载 CSV 文件
|
* 加载 CSV 文件
|
||||||
* @template T 返回数据的类型,默认为 Record<string, string>
|
* @template T 返回数据的类型,默认为 Record<string, string>
|
||||||
* @param pathOrContent 文件路径或 inline CSV 字符串
|
|
||||||
* @returns 解析后的 CSV 数据
|
|
||||||
*/
|
*/
|
||||||
export async function loadCSV<T = Record<string, string>>(pathOrContent: string): Promise<CSV<T>> {
|
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> {
|
||||||
// 检测是否是 inline CSV 数据
|
// 尝试从索引获取
|
||||||
if (isCSV(pathOrContent)) {
|
const content = await getIndexedData(path);
|
||||||
return parseCSVString<T>(pathOrContent, 'inline');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从索引获取文件内容
|
|
||||||
const content = await getIndexedData(pathOrContent);
|
|
||||||
|
|
||||||
// 解析 front matter
|
// 解析 front matter
|
||||||
const { frontmatter, remainingContent } = parseFrontMatter(content);
|
const { frontmatter, remainingContent } = parseFrontMatter(content);
|
||||||
|
|
@ -132,7 +58,7 @@ export async function loadCSV<T = Record<string, string>>(pathOrContent: string)
|
||||||
Object.assign(each, frontmatter);
|
Object.assign(each, frontmatter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
csvResult.sourcePath = pathOrContent;
|
csvResult.sourcePath = path;
|
||||||
return csvResult;
|
return csvResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,5 +84,5 @@ export function processVariables<T extends JSONObject> (body: string, currentRow
|
||||||
return `{{${key}}}`;
|
return `{{${key}}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return body?.replace(/\{\{([^}]+)\}\}/g, (_, key) => `${replaceProp(key)}`) || '';
|
return body?.replace(/\{\{(\w+)\}\}/g, (_, key) => `${replaceProp(key)}`) || '';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
/**
|
|
||||||
* 解析整数范围字符串
|
|
||||||
* @param label 标签字符串,可能是单个整数(如 "1")或整数范围(如 "1-3")
|
|
||||||
* @returns 如果成功解析返回 [min, max],否则返回 null
|
|
||||||
*/
|
|
||||||
export function parseIntegerRange(label: string): [number, number] | null {
|
|
||||||
const trimmed = label.trim();
|
|
||||||
|
|
||||||
// 尝试匹配整数范围格式 (如 "1-3")
|
|
||||||
const rangeMatch = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
||||||
if (rangeMatch) {
|
|
||||||
const min = parseInt(rangeMatch[1]!, 10);
|
|
||||||
const max = parseInt(rangeMatch[2]!, 10);
|
|
||||||
if (min <= max) {
|
|
||||||
return [min, max];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试匹配单个整数格式 (如 "5")
|
|
||||||
const intMatch = trimmed.match(/^(\d+)$/);
|
|
||||||
if (intMatch) {
|
|
||||||
const num = parseInt(intMatch[1]!, 10);
|
|
||||||
return [num, num];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算加权随机的总权重
|
|
||||||
* @param labels 标签数组
|
|
||||||
* @returns 总权重值
|
|
||||||
*/
|
|
||||||
export function calculateTotalWeight(labels: string[]): number {
|
|
||||||
let total = 0;
|
|
||||||
for (const label of labels) {
|
|
||||||
const range = parseIntegerRange(label);
|
|
||||||
if (range) {
|
|
||||||
const [min, max] = range;
|
|
||||||
total += max - min + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据权重随机选择一个索引
|
|
||||||
* @param labels 标签数组
|
|
||||||
* @param totalWeight 总权重(可选,会自行计算)
|
|
||||||
* @returns 选中的索引,如果没有有效权重则返回 -1
|
|
||||||
*/
|
|
||||||
export function weightedRandomIndex(labels: string[], totalWeight?: number): number {
|
|
||||||
const weight = totalWeight ?? calculateTotalWeight(labels);
|
|
||||||
|
|
||||||
if (weight === 0) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成一个随机权重值 (1 到 totalWeight)
|
|
||||||
const roll = Math.floor(Math.random() * weight) + 1;
|
|
||||||
|
|
||||||
let cumulative = 0;
|
|
||||||
for (let i = 0; i < labels.length; i++) {
|
|
||||||
const range = parseIntegerRange(labels[i]!);
|
|
||||||
if (range) {
|
|
||||||
const [min, max] = range;
|
|
||||||
const labelWeight = max - min + 1;
|
|
||||||
cumulative += labelWeight;
|
|
||||||
|
|
||||||
if (roll <= cumulative) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 理论上不会到这里,但为了安全返回最后一个
|
|
||||||
return labels.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查所有 label 是否都是整数或整数范围格式
|
|
||||||
* @param labels 标签数组
|
|
||||||
* @returns 如果所有 label 都是整数/整数范围格式返回 true
|
|
||||||
*/
|
|
||||||
export function areAllLabelsNumeric(labels: string[]): boolean {
|
|
||||||
if (labels.length === 0) return false;
|
|
||||||
return labels.every(label => parseIntegerRange(label) !== null);
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { Marked, type MarkedExtension } from 'marked';
|
import { Marked } from 'marked';
|
||||||
import {createDirectives, presetDirectiveConfigs} from 'marked-directive';
|
import {createDirectives, presetDirectiveConfigs} from 'marked-directive';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import markedAlert from "marked-alert";
|
import markedAlert from "marked-alert";
|
||||||
import markedMermaid from "./mermaid";
|
import markedMermaid from "./mermaid";
|
||||||
import markedTable from "./table";
|
|
||||||
import {gfmHeadingId} from "marked-gfm-heading-id";
|
import {gfmHeadingId} from "marked-gfm-heading-id";
|
||||||
|
|
||||||
let globalIconPrefix: string | undefined = undefined;
|
let globalIconPrefix: string | undefined = undefined;
|
||||||
|
|
@ -15,13 +14,11 @@ function overrideIconPrefix(path?: string){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 marked-directive 来支持指令语法
|
// 使用 marked-directive 来支持指令语法
|
||||||
const marked = new Marked()
|
const marked = new Marked()
|
||||||
.use(gfmHeadingId())
|
.use(gfmHeadingId())
|
||||||
.use(markedAlert())
|
.use(markedAlert())
|
||||||
.use(markedMermaid())
|
.use(markedMermaid())
|
||||||
.use(markedTable())
|
|
||||||
.use(createDirectives([
|
.use(createDirectives([
|
||||||
...presetDirectiveConfigs,
|
...presetDirectiveConfigs,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import type { MarkedExtension, Tokens } from "marked";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将表格数据转换为 CSV 格式字符串
|
|
||||||
* @param headers 表头数组
|
|
||||||
* @param rows 表格数据行
|
|
||||||
* @returns CSV 格式字符串
|
|
||||||
*/
|
|
||||||
function tableToCSV(headers: string[], rows: string[][]): string {
|
|
||||||
const escapeCell = (cell: string) => {
|
|
||||||
// 如果单元格包含逗号、换行或引号,需要转义
|
|
||||||
if (cell.includes(',') || cell.includes('\n') || cell.includes('"')) {
|
|
||||||
return `"${cell.replace(/"/g, '""')}"`;
|
|
||||||
}
|
|
||||||
return cell;
|
|
||||||
};
|
|
||||||
|
|
||||||
const headerLine = headers.map(escapeCell).join(',');
|
|
||||||
const dataLines = rows.map(row => row.map(escapeCell).join(','));
|
|
||||||
|
|
||||||
return [headerLine, ...dataLines].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function markedTable(): MarkedExtension {
|
|
||||||
return {
|
|
||||||
renderer: {
|
|
||||||
table(token: Tokens.Table) {
|
|
||||||
// 检查表头是否包含 md-table-label
|
|
||||||
const header = token.header;
|
|
||||||
const labelIndex = header.findIndex(cell => cell.text === 'md-table-label');
|
|
||||||
|
|
||||||
// 默认表格渲染 - 使用 marked 默认行为
|
|
||||||
if(labelIndex === -1) return false;
|
|
||||||
|
|
||||||
const headers = token.header.map(cell => cell.text);
|
|
||||||
headers[labelIndex] = 'label';
|
|
||||||
const rows = token.rows.map(row => row.map(cell => cell.text));
|
|
||||||
|
|
||||||
if(header.findIndex(header => header.text === 'body') < 0){
|
|
||||||
// 收集所有非 label 列的表头
|
|
||||||
const bodyColumns = header
|
|
||||||
.filter(cell => cell.text !== 'md-table-label')
|
|
||||||
.map(cell => cell.text);
|
|
||||||
|
|
||||||
// 构建 body 列的模板:**列名**:{{列名}}\n\n
|
|
||||||
const bodyTemplate = bodyColumns
|
|
||||||
.map(col => `**${col}**:{{${col}}}`)
|
|
||||||
.join('\n\n');
|
|
||||||
|
|
||||||
headers.push('body');
|
|
||||||
rows.forEach(row => {
|
|
||||||
row.push(bodyTemplate);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成 CSV 数据
|
|
||||||
const csvData = tableToCSV(headers, rows);
|
|
||||||
|
|
||||||
// 渲染为 md-table 组件,内联 CSV 数据
|
|
||||||
return `<md-table>${csvData}</md-table>\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue