Skip to content

Commit

Permalink
feat: playground
Browse files Browse the repository at this point in the history
  • Loading branch information
ikkz committed Feb 7, 2025
1 parent 974925f commit c23ca3e
Show file tree
Hide file tree
Showing 26 changed files with 2,502 additions and 308 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
version: '0.4.27'
- name: Package Test
run: pnpm run package
- name: Build playground
run: pnpm run pg-build

- name: Upload APKGs
uses: actions/[email protected]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm exec playwright test
run: pnpm e2e-test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
Expand Down
8 changes: 7 additions & 1 deletion build/entries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BuildConfig } from './config';
import type { BuildConfig } from './config.ts';

export interface Note<F extends string> {
config: Partial<BuildConfig>;
Expand All @@ -7,6 +7,7 @@ export interface Note<F extends string> {

interface Entry<F extends readonly string[]> {
fields: F;
desc: string;
notes: Note<F[number]>[];
}

Expand All @@ -20,6 +21,7 @@ const mdQuestion =
"## Markdown Basic Syntax<br><br>I just love **bold text**. Italicized text is the _cat's meow_. At the command prompt, type `nano`.<br><br>My favorite markdown editor is [ByteMD](https://github.com/bytedance/bytemd).<br><br>1. First item<br>2. Second item<br>3. Third item<br><br>&gt; Dorothy followed her through many of the beautiful rooms in her castle.<br><br>```js<br>import gfm from '@bytemd/plugin-gfm'<br>import { Editor, Viewer } from 'bytemd'<br><br>const plugins = [<br>&nbsp; gfm(),<br>&nbsp; // Add more plugins here<br>]<br><br>const editor = new Editor({<br>&nbsp; target: document.body, // DOM to render<br>&nbsp; props: {<br>&nbsp;&nbsp;&nbsp; value: '',<br>&nbsp;&nbsp;&nbsp; plugins,<br>&nbsp; },<br>})<br><br>editor.on('change', (e) =&gt; {<br>&nbsp; editor.$set({ value: e.detail.value })<br>})<br>```<br><br>## GFM Extended Syntax<br><br>Automatic URL Linking: <a href=\"https://github.com/bytedance/bytemd\">https://github.com/bytedance/bytemd</a><br><br>~~The world is flat.~~ We now know that the world is round.<br><br>- [x] Write the press release<br>- [ ] Update the website<br>- [ ] Contact the media<br><br>| Syntax&nbsp;&nbsp;&nbsp; | Description |<br>| --------- | ----------- |<br>| Header&nbsp;&nbsp;&nbsp; | Title&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |<br>| Paragraph | Text&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |<br><br>## Math Equation<br><br>Inline math equation: $a+b$<br><br>$$<br>\\displaystyle \\left( \\sum_{k=1}^n a_k b_k \\right)^2 \\leq \\left( \\sum_{k=1}^n a_k^2 \\right) \\left( \\sum_{k=1}^n b_k^2 \\right)<br>$$<br><br>## Mermaid Diagrams<br><br>```mermaid<br><div>mindmap<br>&nbsp; root((mindmap))<br>&nbsp;&nbsp;&nbsp; Origins<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Long history<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ::icon(fa fa-book)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Popularisation<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; British popular psychology author Tony Buzan<br>&nbsp;&nbsp;&nbsp; Research<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; On effectiveness&lt;br/&gt;and features<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; On Automatic creation<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Uses<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Creative techniques<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Strategic planning<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Argument mapping<br>&nbsp;&nbsp;&nbsp; Tools<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Pen and paper<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Mermaid<br><br></div>```";

const mcq = defineEntry({
desc: 'Multiple choice question (6 options)',
fields: [
'question',
'optionA',
Expand Down Expand Up @@ -74,6 +76,7 @@ const mcq = defineEntry({
});

const mcq_10 = defineEntry({
desc: 'Multiple choice question (10 options)',
fields: [
'question',
'optionA',
Expand Down Expand Up @@ -107,6 +110,7 @@ const entries = {
mcq,
mcq_10,
basic: defineEntry({
desc: 'Basic Q&A',
fields: ['question', 'answer', 'note', 'Tags'],
notes: [
{
Expand All @@ -133,6 +137,7 @@ const entries = {
],
}),
tf: defineEntry({
desc: 'True or false',
fields: ['question', 'items', 'note', 'Tags'],
notes: [
{
Expand Down Expand Up @@ -161,6 +166,7 @@ const entries = {
],
}),
match: defineEntry({
desc: 'Drag and drop interactive matching',
fields: ['question', 'items', 'note', 'Tags'],
notes: [
{
Expand Down
3 changes: 1 addition & 2 deletions build/plugins/generate-template.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { BuildConfig } from '../config.ts';
import { entries, type Note } from '../entries.ts';
import { findMatchNote } from '../utils.ts';
import { extname } from 'node:path';
import * as R from 'remeda';
import type { Plugin } from 'rollup';

Expand All @@ -26,7 +25,7 @@ export default (config: BuildConfig) =>
name: 'generate-template',
generateBundle(_, bundle) {
Object.keys(bundle)
.filter((fileName) => extname(fileName) !== '.html')
.filter((fileName) => fileName.split('.').pop() !== 'html')
.forEach((fileName) => {
delete bundle[fileName];
});
Expand Down
9 changes: 6 additions & 3 deletions build/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { BuildConfig } from './config.ts';
import { entries } from './entries.ts';
import devServer from './plugins/dev-server/index.ts';
import generateTemplate from './plugins/generate-template.ts';
import { readJson, ensureValue, findMatchNote } from './utils.ts';
import { ensureValue, findMatchNote } from './utils.ts';
import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import html from '@rollup/plugin-html';
Expand All @@ -25,10 +25,13 @@ import type {
import nodePolyfills from 'rollup-plugin-polyfill-node';
import postcss from 'rollup-plugin-postcss';
import { swc, minify } from 'rollup-plugin-swc3';
// import { visualizer } from 'rollup-plugin-visualizer';
import tailwindcss from 'tailwindcss';

const packageJson = await readJson('./package.json');
const packageJson = JSON.parse(
await fs.readFile(path.resolve(import.meta.dirname, '../package.json'), {
encoding: 'utf8',
}),
);

export async function rollupOptions(
config: BuildConfig,
Expand Down
18 changes: 8 additions & 10 deletions build/utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import type { BuildConfig } from './config';
import type { BuildConfig } from './config.ts';
import { entries } from './entries.ts';
import fs from 'node:fs/promises';
import { template } from 'lodash-es';

export function ensureValue<T>(value: T): T extends () => infer R ? R : T {
return typeof value === 'function' ? value() : value;
}

export async function readJson(path: string) {
return JSON.parse(
await fs.readFile(path, {
encoding: 'utf-8',
}),
);
}

export function configMatch(
pattern: Partial<BuildConfig>,
config: BuildConfig,
Expand All @@ -29,3 +21,9 @@ export function findMatchNote(config: BuildConfig) {
configMatch(noteConfig, config),
);
}

export function renderTemplate(html: string, data: object) {
return template(html, {
interpolate: /{{([\s\S]+?)}}/g,
})(data);
}
8 changes: 1 addition & 7 deletions e2e/anki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {
type BuildJson,
BUILTIN_FIELDS,
} from '../build/plugins/generate-template';
import { renderTemplate } from '../build/utils';
import { readTemplate } from './utils';
import { type Page } from '@playwright/test';
import { template } from 'lodash-es';

declare const e2eAnki: {
clean(): void;
Expand Down Expand Up @@ -67,9 +67,3 @@ export class Anki {
});
}
}

function renderTemplate(html: string, data: object) {
return template(html, {
interpolate: /{{([\s\S]+?)}}/g,
})(data);
}
23 changes: 18 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
"lint": "pnpm eslint .",
"format": "pnpm eslint --fix .",
"package": "uv run --frozen build/package.py",
"typecheck": "tsc",
"test-e2e": "pnpm exec playwright test",
"serve-e2e": "pnpm exec serve -p 3001 e2e",
"test": "vitest"
"typecheck": "tsc --build --noEmit",
"e2e-test": "pnpm exec playwright test",
"e2e-serve": "pnpm exec serve -p 3001 e2e",
"test": "vitest",
"pg-build": "vite build --config playground/vite.config.ts",
"pg-dev": "vite --config playground/vite.config.ts",
"pg-preview": "vite preview --config playground/vite.config.ts"
},
"author": "",
"license": "ISC",
Expand Down Expand Up @@ -44,7 +47,9 @@
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.5.3",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.5",
"@types/webfontloader": "^1.6.38",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"cssnano": "^7.0.5",
"eslint": "^9.9.1",
Expand All @@ -55,7 +60,7 @@
"jsdom": "^25.0.1",
"koa": "^2.15.3",
"pnpm": "^9.9.0",
"postcss": "^8.4.44",
"postcss": "^8.5.1",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"rollup": "^4.21.2",
Expand All @@ -68,9 +73,12 @@
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.3",
"typescript-eslint": "^8.4.0",
"vite": "^6.1.0",
"vite-plugin-pages": "^0.32.4",
"vitest": "^2.1.8"
},
"dependencies": {
"@ant-design/icons": "^5.6.0",
"@bytemd/plugin-breaks": "^1.21.0",
"@bytemd/plugin-gfm": "^1.21.0",
"@bytemd/plugin-highlight": "^1.21.0",
Expand All @@ -81,6 +89,7 @@
"@formkit/auto-animate": "^0.8.2",
"ahooks": "^3.8.1",
"anki-storage": "^1.0.2",
"antd": "^5.23.4",
"bytemd": "^1.21.0",
"clsx": "^2.1.1",
"jotai": "^2.11.0",
Expand All @@ -89,7 +98,11 @@
"lucide-react": "^0.468.0",
"mermaid": "^11.4.1",
"preact": "^10.23.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.1.1",
"react-router": "^7.1.5",
"react-router-dom": "^7.1.5",
"react-use": "^17.5.1",
"remeda": "^2.12.0"
}
Expand Down
12 changes: 12 additions & 0 deletions playground/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ikkz Template Playground</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
1 change: 1 addition & 0 deletions playground/public/builds
1 change: 1 addition & 0 deletions playground/public/releases
18 changes: 18 additions & 0 deletions playground/src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { GithubOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { FC } from 'react';

export const Header: FC = () => {
return (
<h1 className="font-bold text-2xl mb-6">
ikkz Template
<Button
icon={<GithubOutlined />}
type="text"
target="_blank"
className="text-xl ml-2"
href="https://github.com/ikkz/anki-template"
/>
</h1>
);
};
78 changes: 78 additions & 0 deletions playground/src/components/previewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
BuildJson,
BUILTIN_FIELDS,
} from '../../../build/plugins/generate-template';
import { renderTemplate } from '../../../build/utils';
import hostPage from '../../../e2e/index.html?raw';
import { useRequest } from 'ahooks';
import { Spin } from 'antd';
import { FC, useEffect, useRef } from 'react';

declare global {
interface Window {
e2eAnki: {
clean(): void;
flipToBack(): void;
render(html: string): void;
};
}
}

export const Previewer: FC<{ template: string }> = ({ template }) => {
const { data, loading } = useRequest(
() =>
Promise.all([
fetch(`/builds/${template}/front.html`).then((res) => res.text()),
fetch(`/builds/${template}/back.html`).then((res) => res.text()),
fetch(`/builds/${template}/build.json`).then(
(res) => res.json() as Promise<BuildJson>,
),
]),
{
refreshDeps: [template],
},
);

const iframe = useRef<HTMLIFrameElement>(null);

useEffect(() => {
const wind = iframe.current?.contentWindow;
if (!data || !wind) {
return;
}
setTimeout(() => {
const [front, back, build] = data;
const extraFields = {};
const fields = Object.assign(
{},
Object.fromEntries(
[...build.fields, ...BUILTIN_FIELDS].map((k) => [k, '']),
),
build.notes[0].fields,
extraFields,
);
const frontHtml = renderTemplate(front, fields);
const backHtml = renderTemplate(back, {
...fields,
FrontSide: frontHtml,
});
wind.e2eAnki.render(frontHtml);
wind.e2eAnki.flipToBack = () => {
wind.e2eAnki.render(backHtml);
};
}, 500);
}, [data]);

if (loading) {
return <Spin className="h-full flex items-center justify-center" />;
}

return (
<iframe
key={template}
srcDoc={hostPage}
className="border-none h-screen md:h-full w-full"
ref={iframe}
/>
);
};
Loading

0 comments on commit c23ca3e

Please sign in to comment.