Compare commits
3 Commits
6fcd879287
...
27bc2fc747
| Author | SHA1 | Date |
|---|---|---|
|
|
27bc2fc747 | |
|
|
2e04934881 | |
|
|
c668dce348 |
|
|
@ -127,7 +127,7 @@ export function PrintPreview(props: PrintPreviewProps) {
|
||||||
width={`${store.state.dimensions?.cardWidth || 56}mm`}
|
width={`${store.state.dimensions?.cardWidth || 56}mm`}
|
||||||
height={`${store.state.dimensions?.cardHeight || 88}mm`}
|
height={`${store.state.dimensions?.cardHeight || 88}mm`}
|
||||||
>
|
>
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml" class="w-full h-full bg-white">
|
<div class="w-full h-full bg-white" {...({ xmlns: 'http://www.w3.org/1999/xhtml' } as any)}>
|
||||||
<div
|
<div
|
||||||
class="absolute"
|
class="absolute"
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ interface TableRow {
|
||||||
|
|
||||||
customElement('md-table', { roll: false, remix: false }, (props, { element }) => {
|
customElement('md-table', { roll: false, remix: false }, (props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
const [rows, setRows] = createSignal<CSV<TableRow>>([]);
|
const [rows, setRows] = createSignal<CSV<TableRow>>([] as unknown as CSV<TableRow>);
|
||||||
const [activeTab, setActiveTab] = createSignal(0);
|
const [activeTab, setActiveTab] = createSignal(0);
|
||||||
const [activeGroup, setActiveGroup] = createSignal<string | null>(null);
|
const [activeGroup, setActiveGroup] = createSignal<string | null>(null);
|
||||||
const [bodyHtml, setBodyHtml] = createSignal('');
|
const [bodyHtml, setBodyHtml] = createSignal('');
|
||||||
|
|
@ -45,7 +45,8 @@ customElement('md-table', { roll: false, remix: false }, (props, { element }) =>
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const data = csvData();
|
const data = csvData();
|
||||||
if (data) {
|
if (data) {
|
||||||
setRows(data as any[]);
|
// 将加载的数据赋值给 rows,CSV 类型已经包含 sourcePath 等属性
|
||||||
|
setRows(data as unknown as CSV<TableRow>);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { For, Show, createEffect } from 'solid-js';
|
||||||
import type {TextResult, RuntimeResult, OptionsResult} from '../yarn-spinner/runtime/results';
|
import type {TextResult, RuntimeResult, OptionsResult} from '../yarn-spinner/runtime/results';
|
||||||
import { createYarnStore } from './stores/yarnStore';
|
import { createYarnStore } from './stores/yarnStore';
|
||||||
|
|
||||||
|
export interface YarnSpinnerProps {
|
||||||
|
start: string;
|
||||||
|
}
|
||||||
|
|
||||||
customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { element }) => {
|
customElement<{start: string}>('md-yarn-spinner', {start: 'start'}, (props, { element }) => {
|
||||||
noShadowDOM();
|
noShadowDOM();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {loadElementSrc, resolvePath} from "../utils/path";
|
||||||
import {createStore} from "solid-js/store";
|
import {createStore} from "solid-js/store";
|
||||||
import {RunnerOptions} from "../../yarn-spinner/runtime/runner";
|
import {RunnerOptions} from "../../yarn-spinner/runtime/runner";
|
||||||
import {getTtrpgFunctions} from "./ttrpgRunner";
|
import {getTtrpgFunctions} from "./ttrpgRunner";
|
||||||
|
import { getIndexedData } from "../../data-loader/file-index";
|
||||||
|
|
||||||
type YarnSpinnerStore = {
|
type YarnSpinnerStore = {
|
||||||
dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[],
|
dialogueHistory: (RuntimeResult | OptionsResult['options'][0])[],
|
||||||
|
|
@ -43,8 +44,8 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
|
||||||
startAt: props.start,
|
startAt: props.start,
|
||||||
});
|
});
|
||||||
const {commands, functions} = getTtrpgFunctions(runner);
|
const {commands, functions} = getTtrpgFunctions(runner);
|
||||||
runner.registerFunctions(functions);
|
|
||||||
runner.registerCommands(commands);
|
runner.registerCommands(commands);
|
||||||
|
runner.registerFunctions(functions);
|
||||||
return runner;
|
return runner;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize YarnRunner:', error);
|
console.error('Failed to initialize YarnRunner:', error);
|
||||||
|
|
@ -108,22 +109,8 @@ export function createYarnStore(element: HTMLElement, props: {start: string}){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存已加载的 yarn 文件内容
|
|
||||||
const yarnCache = new Map<string, string>();
|
|
||||||
|
|
||||||
// 加载 yarn 文件内容
|
|
||||||
async function loadYarnFile(path: string): Promise<string> {
|
|
||||||
if (yarnCache.has(path)) {
|
|
||||||
return yarnCache.get(path)!;
|
|
||||||
}
|
|
||||||
const response = await fetch(path);
|
|
||||||
const content = await response.text();
|
|
||||||
yarnCache.set(path, content);
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载多个 yarn 文件并拼接
|
// 加载多个 yarn 文件并拼接
|
||||||
async function loadYarnFiles(paths: string[]): Promise<string> {
|
async function loadYarnFiles(paths: string[]): Promise<string> {
|
||||||
const contents = await Promise.all(paths.map(path => loadYarnFile(path)));
|
const contents = await Promise.all(paths.map(path => getIndexedData(path)));
|
||||||
return contents.join('\n');
|
return contents.join('\n');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
import { parse } from 'csv-parse/browser/esm/sync';
|
import { parse } from 'csv-parse/browser/esm/sync';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
|
import { getIndexedData } from '../../data-loader/file-index';
|
||||||
/**
|
|
||||||
* 全局缓存已加载的 CSV 内容
|
|
||||||
*/
|
|
||||||
const csvCache = new Map<string, Record<string, string>[]>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析 front matter
|
* 解析 front matter
|
||||||
|
|
@ -45,12 +41,8 @@ function parseFrontMatter(content: string): { frontmatter?: JSONObject; remainin
|
||||||
* @template T 返回数据的类型,默认为 Record<string, string>
|
* @template T 返回数据的类型,默认为 Record<string, string>
|
||||||
*/
|
*/
|
||||||
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> {
|
export async function loadCSV<T = Record<string, string>>(path: string): Promise<CSV<T>> {
|
||||||
if (csvCache.has(path)) {
|
// 尝试从索引获取
|
||||||
return csvCache.get(path)! as CSV<T>;
|
const content = await getIndexedData(path);
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(path);
|
|
||||||
const content = await response.text();
|
|
||||||
|
|
||||||
// 解析 front matter
|
// 解析 front matter
|
||||||
const { frontmatter, remainingContent } = parseFrontMatter(content);
|
const { frontmatter, remainingContent } = parseFrontMatter(content);
|
||||||
|
|
@ -72,8 +64,6 @@ export async function loadCSV<T = Record<string, string>>(path: string): Promise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
csvResult.sourcePath = path;
|
csvResult.sourcePath = path;
|
||||||
|
|
||||||
csvCache.set(path, result);
|
|
||||||
return csvResult;
|
return csvResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
/**
|
||||||
|
* 文件索引管理器
|
||||||
|
* 支持任意文件类型的索引加载和缓存
|
||||||
|
*/
|
||||||
|
|
||||||
|
type FileIndex = Record<string, string>;
|
||||||
|
|
||||||
|
let fileIndex: FileIndex | null = null;
|
||||||
|
let indexLoadPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载文件索引(只加载一次)
|
||||||
|
* 支持 CLI 环境(从 JSON 加载)和 Dev 环境(使用 webpackContext 仅加载 .md 文件)
|
||||||
|
*/
|
||||||
|
function ensureIndexLoaded(): Promise<void> {
|
||||||
|
if (indexLoadPromise) return indexLoadPromise;
|
||||||
|
|
||||||
|
indexLoadPromise = (async () => {
|
||||||
|
// 尝试 CLI 环境:从 /__FILE_INDEX.json 加载
|
||||||
|
try {
|
||||||
|
const response = await fetch("/__FILE_INDEX.json");
|
||||||
|
if (response.ok) {
|
||||||
|
const index = await response.json();
|
||||||
|
fileIndex = { ...fileIndex, ...index };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// CLI 索引不可用时尝试 dev 环境
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dev 环境:使用 import.meta.webpackContext + raw loader 加载 .md, .csv, .yarn 文件
|
||||||
|
try {
|
||||||
|
const context = import.meta.webpackContext("../../content", {
|
||||||
|
recursive: true,
|
||||||
|
regExp: /\.md|\.yarn|\.csv$/i,
|
||||||
|
});
|
||||||
|
const keys = context.keys();
|
||||||
|
const index: FileIndex = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
// context 返回的是模块,需要访问其 default 导出(raw-loader 处理后的内容)
|
||||||
|
const module = context(key) as { default?: string } | string;
|
||||||
|
const content = typeof module === "string" ? module : module.default ?? "";
|
||||||
|
const normalizedPath = "/content" + key.slice(1);
|
||||||
|
index[normalizedPath] = content;
|
||||||
|
}
|
||||||
|
fileIndex = { ...fileIndex, ...index };
|
||||||
|
} catch (e) {
|
||||||
|
// webpackContext 不可用时忽略
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return indexLoadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从索引获取文件内容
|
||||||
|
*/
|
||||||
|
export async function getIndexedData(path: string): Promise<string> {
|
||||||
|
await ensureIndexLoaded();
|
||||||
|
if (fileIndex && fileIndex[path]) {
|
||||||
|
return fileIndex[path];
|
||||||
|
}
|
||||||
|
const res = await fetch(path);
|
||||||
|
const content = await res.text();
|
||||||
|
fileIndex = fileIndex || {};
|
||||||
|
fileIndex[path] = content;
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定扩展名的文件路径
|
||||||
|
*/
|
||||||
|
export async function getPathsByExtension(ext: string): Promise<string[]> {
|
||||||
|
await ensureIndexLoaded();
|
||||||
|
if (!fileIndex) return [];
|
||||||
|
const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`;
|
||||||
|
return Object.keys(fileIndex).filter(path =>
|
||||||
|
path.toLowerCase().endsWith(normalizedExt.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除索引(用于测试或重新加载)
|
||||||
|
*/
|
||||||
|
export function clearIndex(): void {
|
||||||
|
fileIndex = null;
|
||||||
|
indexLoadPromise = null;
|
||||||
|
}
|
||||||
|
|
@ -1,83 +1,19 @@
|
||||||
import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc";
|
import { buildFileTree, extractHeadings, extractSection, type TocNode, type FileNode } from "./toc";
|
||||||
|
import {getIndexedData, getPathsByExtension} from "./file-index";
|
||||||
|
|
||||||
export { TocNode, FileNode, extractHeadings, buildFileTree, extractSection };
|
export { TocNode, FileNode, extractHeadings, buildFileTree, extractSection };
|
||||||
|
|
||||||
let dataIndex: Record<string, string> | null = null;
|
|
||||||
let indexLoadPromise: Promise<void> | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从索引获取数据
|
|
||||||
*/
|
|
||||||
function getIndexedData(path: string): string | null {
|
|
||||||
if (dataIndex && dataIndex[path]) {
|
|
||||||
return dataIndex[path];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有索引路径
|
|
||||||
*/
|
|
||||||
export function getIndexedPaths(): string[] {
|
|
||||||
if (!dataIndex) return [];
|
|
||||||
return Object.keys(dataIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载索引(只加载一次)
|
|
||||||
*/
|
|
||||||
export function ensureIndexLoaded(): Promise<void> {
|
|
||||||
if (indexLoadPromise) return indexLoadPromise;
|
|
||||||
|
|
||||||
indexLoadPromise = (async () => {
|
|
||||||
// 尝试 CLI 环境:从 /__CONTENT_INDEX.json 加载
|
|
||||||
try {
|
|
||||||
const response = await fetch("/__CONTENT_INDEX.json");
|
|
||||||
if (response.ok) {
|
|
||||||
const index = await response.json();
|
|
||||||
dataIndex = { ...dataIndex, ...index };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// CLI 索引不可用时尝试 dev 环境
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dev 环境:使用 import.meta.webpackContext + raw loader 加载
|
|
||||||
try {
|
|
||||||
const context = import.meta.webpackContext("../../content", {
|
|
||||||
recursive: true,
|
|
||||||
regExp: /\.md$/,
|
|
||||||
});
|
|
||||||
const keys = context.keys();
|
|
||||||
const index: Record<string, string> = {};
|
|
||||||
for (const key of keys) {
|
|
||||||
// context 返回的是模块,需要访问其 default 导出(raw-loader 处理后的内容)
|
|
||||||
const module = context(key) as { default?: string } | string;
|
|
||||||
const content = typeof module === "string" ? module : module.default ?? "";
|
|
||||||
const normalizedPath = "/content" + key.slice(1);
|
|
||||||
index[normalizedPath] = content;
|
|
||||||
}
|
|
||||||
dataIndex = { ...dataIndex, ...index };
|
|
||||||
} catch (e) {
|
|
||||||
// webpackContext 不可用时忽略
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return indexLoadPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成目录树(文件树 + 标题结构)
|
* 生成目录树(文件树 + 标题结构)
|
||||||
|
* 仅处理 .md 文件
|
||||||
*/
|
*/
|
||||||
export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeadings: Record<string, TocNode[]> }> {
|
export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeadings: Record<string, TocNode[]> }> {
|
||||||
await ensureIndexLoaded();
|
const mdPaths = await getPathsByExtension("md");
|
||||||
|
const fileTree = buildFileTree(mdPaths);
|
||||||
const paths = getIndexedPaths();
|
|
||||||
const fileTree = buildFileTree(paths);
|
|
||||||
const pathHeadings: Record<string, TocNode[]> = {};
|
const pathHeadings: Record<string, TocNode[]> = {};
|
||||||
|
|
||||||
for (const path of paths) {
|
for (const path of mdPaths) {
|
||||||
const content = getIndexedData(path);
|
const content = await getIndexedData(path);
|
||||||
if (content) {
|
if (content) {
|
||||||
pathHeadings[path] = extractHeadings(content);
|
pathHeadings[path] = extractHeadings(content);
|
||||||
}
|
}
|
||||||
|
|
@ -85,31 +21,3 @@ export async function generateToc(): Promise<{ fileTree: FileNode[]; pathHeading
|
||||||
|
|
||||||
return { fileTree, pathHeadings };
|
return { fileTree, pathHeadings };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 异步加载数据
|
|
||||||
* @param path 数据路径
|
|
||||||
* @returns 数据内容
|
|
||||||
*/
|
|
||||||
export async function fetchData(path: string): Promise<string> {
|
|
||||||
await ensureIndexLoaded();
|
|
||||||
|
|
||||||
// 首先尝试从索引获取
|
|
||||||
const indexedData = getIndexedData(path);
|
|
||||||
if (indexedData) {
|
|
||||||
return indexedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 索引不存在时,使用 fetch 加载
|
|
||||||
if (dataIndex !== null) throw new Error(`no data in index: ${path}`);
|
|
||||||
try {
|
|
||||||
const response = await fetch(path);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch: ${path}`);
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("fetchData error:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface WebpackContext {
|
||||||
|
(path: string): { default?: string } | string;
|
||||||
|
keys(): string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
webpackContext(
|
||||||
|
directory: string,
|
||||||
|
options: {
|
||||||
|
recursive?: boolean;
|
||||||
|
regExp?: RegExp;
|
||||||
|
}
|
||||||
|
): WebpackContext;
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
/* icon */
|
/* icon */
|
||||||
icon, pull{
|
icon, pull{
|
||||||
@apply inline-block;
|
@apply inline-block;
|
||||||
width: 1em;
|
width: 2em;
|
||||||
height: 1.27em;
|
height: 2em;
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
--icon-src: '';
|
--icon-src: '';
|
||||||
background-image: var(--icon-src);
|
background-image: var(--icon-src);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue