This commit is contained in:
Max Richter
2025-09-26 12:42:06 +02:00
parent deae5acac8
commit ae5cd8481a
17 changed files with 649 additions and 233 deletions

View File

@@ -95,7 +95,7 @@ func TestFuzzyBlockMatchSalad(t *testing.T) {
}
blocks, err := template.CompileTemplate(schemaMd)
if err != nil {
t.Errorf("Failed to compile template: %s", err.Error())
t.Errorf("failed to compile template: %s", err.Error())
t.FailNow()
}

View File

@@ -40,19 +40,9 @@ func DetectType(markdownContent string) (string, error) {
return "", fmt.Errorf("could not parse frontmatter")
}
func MatchBlocks(markdownContent string) ([]matcher.Block, error) {
func MatchBlocks(markdownContent, templateContent string) ([]matcher.Block, error) {
markdownContent = strings.TrimSuffix(markdownContent, "\n")
contentType, err := DetectType(markdownContent)
if err != nil {
return nil, fmt.Errorf("could not detect type: %w", err)
}
templateContent, err := registry.GetTemplate(contentType)
if err != nil {
return nil, fmt.Errorf("could not get schema: %w", err)
}
tpl, err := template.CompileTemplate(templateContent)
if err != nil {
return nil, fmt.Errorf("failed to compile template: %w", err)

View File

@@ -1,43 +1,53 @@
{
"name": "playground",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.22.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^22",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^7.0.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
}
"name": "playground",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.22.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^22",
"eslint": "^9.22.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^7.0.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
},
"dependencies": {
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-markdown": "^6.3.4",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.3",
"codemirror": "^6.0.2",
"lucide-svelte": "^0.544.0",
"svelte-codemirror-editor": "^2.0.0"
}
}

View File

@@ -7,6 +7,31 @@ settings:
importers:
.:
dependencies:
'@codemirror/lang-javascript':
specifier: ^6.2.4
version: 6.2.4
'@codemirror/lang-json':
specifier: ^6.0.2
version: 6.0.2
'@codemirror/lang-markdown':
specifier: ^6.3.4
version: 6.3.4
'@codemirror/state':
specifier: ^6.5.2
version: 6.5.2
'@codemirror/view':
specifier: ^6.38.3
version: 6.38.3
codemirror:
specifier: ^6.0.2
version: 6.0.2
lucide-svelte:
specifier: ^0.544.0
version: 0.544.0(svelte@5.39.6)
svelte-codemirror-editor:
specifier: ^2.0.0
version: 2.0.0(codemirror@6.0.2)(svelte@5.39.6)
devDependencies:
'@eslint/compat':
specifier: ^1.2.5
@@ -71,6 +96,42 @@ importers:
packages:
'@codemirror/autocomplete@6.18.7':
resolution: {integrity: sha512-8EzdeIoWPJDsMBwz3zdzwXnUpCzMiCyz5/A3FIPpriaclFCGDkAzK13sMcnsu5rowqiyeQN2Vs2TsOcoDPZirQ==}
'@codemirror/commands@6.8.1':
resolution: {integrity: sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==}
'@codemirror/lang-css@6.3.1':
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
'@codemirror/lang-html@6.4.10':
resolution: {integrity: sha512-h/SceTVsN5r+WE+TVP2g3KDvNoSzbSrtZXCKo4vkKdbfT5t4otuVgngGdFukOO/rwRD2++pCxoh6xD4TEVMkQA==}
'@codemirror/lang-javascript@6.2.4':
resolution: {integrity: sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==}
'@codemirror/lang-json@6.0.2':
resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
'@codemirror/lang-markdown@6.3.4':
resolution: {integrity: sha512-fBm0BO03azXnTAsxhONDYHi/qWSI+uSEIpzKM7h/bkIc9fHnFp9y7KTMXKON0teNT97pFhc1a9DQTtWBYEZ7ug==}
'@codemirror/language@6.11.3':
resolution: {integrity: sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==}
'@codemirror/lint@6.8.5':
resolution: {integrity: sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==}
'@codemirror/search@6.5.11':
resolution: {integrity: sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==}
'@codemirror/state@6.5.2':
resolution: {integrity: sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==}
'@codemirror/view@6.38.3':
resolution: {integrity: sha512-x2t87+oqwB1mduiQZ6huIghjMt4uZKFEdj66IcXw7+a5iBEvv9lh7EWDRHI7crnD4BMGpnyq/RzmCGbiEZLcvQ==}
'@esbuild/aix-ppc64@0.25.10':
resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==}
engines: {node: '>=18'}
@@ -314,6 +375,33 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@lezer/common@1.2.3':
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
'@lezer/css@1.3.0':
resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==}
'@lezer/highlight@1.2.1':
resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==}
'@lezer/html@1.3.11':
resolution: {integrity: sha512-SV04kK5EHDPPecMCiFNZAnQhUIxktP04yHxgOKK7TZ3+KUAlK9f4dcYbjAWwDx2C2pJmiOeSV05QEbHeQo5JqA==}
'@lezer/javascript@1.5.4':
resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
'@lezer/json@1.0.3':
resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
'@lezer/lr@1.4.2':
resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
'@lezer/markdown@1.4.3':
resolution: {integrity: sha512-kfw+2uMrQ/wy/+ONfrH83OkdFNM0ye5Xq96cLlaCy7h5UT9FO54DU4oRoIc0CSBh5NWmWuiIJA7NGLMJbQ+Oxg==}
'@marijn/find-cluster-break@1.0.2':
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -702,6 +790,9 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
codemirror@6.0.2:
resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -716,6 +807,9 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1043,6 +1137,11 @@ packages:
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lucide-svelte@0.544.0:
resolution: {integrity: sha512-8kBxSivf8SJdEUJRHBpu9bRw0S/qfVK+Yfb92KQnRRBdP425RzT6aQfrIfZctG1oucPVTBQe1ZXgmth/3qVICg==}
peerDependencies:
svelte: ^3 || ^4 || ^5.0.0-next.42
magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
@@ -1290,6 +1389,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
style-mod@4.1.2:
resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@@ -1302,6 +1404,12 @@ packages:
svelte: ^4.0.0 || ^5.0.0-next.0
typescript: '>=5.0.0'
svelte-codemirror-editor@2.0.0:
resolution: {integrity: sha512-LJyCPGzOHL290ec4dLBsVyu6e5dsk2pLKUBfU8wA5/EN7Zgc0UXxjNeEsDHGLZhZr2RYMiYwa7tLRZCHOtsxCg==}
peerDependencies:
codemirror: ^6.0.0
svelte: ^5.0.0
svelte-eslint-parser@1.3.3:
resolution: {integrity: sha512-oTrDR8Z7Wnguut7QH3YKh7JR19xv1seB/bz4dxU5J/86eJtZOU4eh0/jZq4dy6tAlz/KROxnkRQspv5ZEt7t+Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1417,6 +1525,9 @@ packages:
vite:
optional: true
w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -1443,6 +1554,97 @@ packages:
snapshots:
'@codemirror/autocomplete@6.18.7':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.3
'@lezer/common': 1.2.3
'@codemirror/commands@6.8.1':
dependencies:
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.3
'@lezer/common': 1.2.3
'@codemirror/lang-css@6.3.1':
dependencies:
'@codemirror/autocomplete': 6.18.7
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@lezer/common': 1.2.3
'@lezer/css': 1.3.0
'@codemirror/lang-html@6.4.10':
dependencies:
'@codemirror/autocomplete': 6.18.7
'@codemirror/lang-css': 6.3.1
'@codemirror/lang-javascript': 6.2.4
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.3
'@lezer/common': 1.2.3
'@lezer/css': 1.3.0
'@lezer/html': 1.3.11
'@codemirror/lang-javascript@6.2.4':
dependencies:
'@codemirror/autocomplete': 6.18.7
'@codemirror/language': 6.11.3
'@codemirror/lint': 6.8.5
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.3
'@lezer/common': 1.2.3
'@lezer/javascript': 1.5.4
'@codemirror/lang-json@6.0.2':
dependencies:
'@codemirror/language': 6.11.3
'@lezer/json': 1.0.3
'@codemirror/lang-markdown@6.3.4':
dependencies:
'@codemirror/autocomplete': 6.18.7
'@codemirror/lang-html': 6.4.10
'@codemirror/language': 6.11.3
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.3
'@lezer/common': 1.2.3
'@lezer/markdown': 1.4.3
'@codemirror/language@6.11.3':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.3
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
style-mod: 4.1.2
'@codemirror/lint@6.8.5':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.3
crelt: 1.0.6
'@codemirror/search@6.5.11':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.3
crelt: 1.0.6
'@codemirror/state@6.5.2':
dependencies:
'@marijn/find-cluster-break': 1.0.2
'@codemirror/view@6.38.3':
dependencies:
'@codemirror/state': 6.5.2
crelt: 1.0.6
style-mod: 4.1.2
w3c-keyname: 2.2.8
'@esbuild/aix-ppc64@0.25.10':
optional: true
@@ -1609,6 +1811,47 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@lezer/common@1.2.3': {}
'@lezer/css@1.3.0':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@lezer/highlight@1.2.1':
dependencies:
'@lezer/common': 1.2.3
'@lezer/html@1.3.11':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@lezer/javascript@1.5.4':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@lezer/json@1.0.3':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@lezer/lr': 1.4.2
'@lezer/lr@1.4.2':
dependencies:
'@lezer/common': 1.2.3
'@lezer/markdown@1.4.3':
dependencies:
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
'@marijn/find-cluster-break@1.0.2': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -1966,6 +2209,16 @@ snapshots:
clsx@2.1.1: {}
codemirror@6.0.2:
dependencies:
'@codemirror/autocomplete': 6.18.7
'@codemirror/commands': 6.8.1
'@codemirror/language': 6.11.3
'@codemirror/lint': 6.8.5
'@codemirror/search': 6.5.11
'@codemirror/state': 6.5.2
'@codemirror/view': 6.38.3
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -1976,6 +2229,8 @@ snapshots:
cookie@0.6.0: {}
crelt@1.0.6: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -2297,6 +2552,10 @@ snapshots:
lodash.merge@4.6.2: {}
lucide-svelte@0.544.0(svelte@5.39.6):
dependencies:
svelte: 5.39.6
magic-string@0.30.19:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -2470,6 +2729,8 @@ snapshots:
strip-json-comments@3.1.1: {}
style-mod@4.1.2: {}
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@@ -2486,6 +2747,11 @@ snapshots:
transitivePeerDependencies:
- picomatch
svelte-codemirror-editor@2.0.0(codemirror@6.0.2)(svelte@5.39.6):
dependencies:
codemirror: 6.0.2
svelte: 5.39.6
svelte-eslint-parser@1.3.3(svelte@5.39.6):
dependencies:
eslint-scope: 8.4.0
@@ -2584,6 +2850,8 @@ snapshots:
optionalDependencies:
vite: 7.1.7(@types/node@22.18.6)(jiti@2.6.0)(lightningcss@1.30.1)
w3c-keyname@2.2.8: {}
which@2.0.2:
dependencies:
isexe: 2.0.0

View File

@@ -1,5 +1,11 @@
<script lang="ts">
import CheckCircleIcon from '$lib/icons/CheckCircleIcon.svelte';
import MinusCircleIcon from '$lib/icons/MinusCircleIcon.svelte';
import XCircleIcon from '$lib/icons/XCircleIcon.svelte';
import type { Extension } from '@codemirror/state';
import { basicSetup } from 'codemirror';
import type { Snippet } from 'svelte';
import CodeMirror from 'svelte-codemirror-editor';
interface Props {
title: string;
@@ -9,9 +15,10 @@
children?: Snippet;
headerActions?: Snippet;
subtitle?: string;
status?: 'success' | 'error';
status?: 'success' | 'error' | 'indeterminate';
timing?: number;
pillText?: string;
langExtension?: Extension;
}
let {
@@ -24,48 +31,27 @@
subtitle,
status,
timing,
pillText
pillText,
langExtension
}: Props = $props();
</script>
<div class="flex h-full flex-col border-r border-gray-200 last:border-r-0">
<div class="flex flex-1 flex-col overflow-hidden border-r border-gray-200 last:border-r-0">
<div class="flex items-center border-b border-gray-200 bg-gray-50/50 px-4 py-3">
{#if status === 'success'}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-5 w-5 text-green-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<CheckCircleIcon class="mr-2 h-5 w-5 text-green-500" />
{:else if status === 'error'}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="mr-2 h-5 w-5 text-red-500"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<XCircleIcon class="mr-2 h-5 w-5 text-red-500" />
{:else if status === 'indeterminate'}
<MinusCircleIcon class="mr-2 h-5 w-5 text-gray-400" />
{/if}
<div class="flex-1">
<h2 class="text-sm font-semibold tracking-wide text-gray-900 uppercase flex items-center">
<h2 class="flex items-center text-sm font-semibold uppercase tracking-wide text-gray-900">
{title}
{#if pillText}
<span class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800">
<span
class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800"
>
{pillText}
</span>
{/if}
@@ -83,22 +69,17 @@
<div class="ml-4 text-xs text-gray-500">{timing}ms</div>
{/if}
</div>
<div class="group relative flex-1">
{#if readonly}
<div class="absolute inset-0 overflow-auto p-4">
<pre
class="font-mono text-sm leading-relaxed whitespace-pre-wrap text-gray-800">{value}</pre>
</div>
{:else}
<textarea
<div class="group relative flex flex-1 flex-col overflow-hidden">
<div class="flex-1 overflow-auto">
<CodeMirror
bind:value
extensions={[basicSetup, langExtension].filter(Boolean) as Extension[]}
{placeholder}
class="absolute inset-0 h-full w-full resize-none border-0 bg-transparent p-4 font-mono text-sm leading-relaxed text-gray-800 transition-colors outline-none placeholder:text-gray-400 focus:bg-gray-50/30"
spellcheck="false"
></textarea>
{/if}
{readonly}
class="text-sm"
/>
</div>
<!-- Subtle hover effect -->
<div
class="pointer-events-none absolute inset-0 bg-black opacity-0 transition-opacity duration-200 group-hover:opacity-5"
></div>

View File

@@ -1,26 +1,21 @@
<script lang="ts">
// import { CodeIcon, ArrowRightIcon } from "lucide-svelte";
import Logo from '$lib/icons/Logo.svelte';
import { GithubIcon } from 'lucide-svelte';
</script>
<header class="sticky top-0 z-10 border-b border-gray-200 bg-white/80 backdrop-blur-sm">
<div class="container mx-auto px-6 py-4">
<div class="flex items-center justify-between">
<div class="container px-6 py-4">
<div class="flex w-full items-center justify-between">
<div class="flex items-center gap-3">
<div class="rounded-lg bg-black p-2">
<!-- <CodeIcon class="h-5 w-5 text-white" /> -->
</div>
<Logo />
<div>
<h1 class="text-2xl font-bold text-black">Marka</h1>
<p class="text-sm text-gray-600">Bidirectional Markdown ↔ JSON Parser</p>
</div>
</div>
<div class="hidden items-center gap-2 text-sm text-gray-500 md:flex">
<span>Template</span>
<!-- <ArrowRightIcon class="w-4 h-4" /> -->
<span>Markdown</span>
<!-- <ArrowRightIcon class="w-4 h-4" /> -->
<span>JSON</span>
</div>
<a href="https://github.com/jim-fx/marka" target="_blank" rel="noopener noreferrer">
<GithubIcon class="h-6 w-6 text-gray-600 transition-colors duration-200 hover:text-black" />
</a>
</div>
</div>
</header>

View File

@@ -1,18 +1,20 @@
<script lang="ts">
import type { ParseResult } from '../wasm';
import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import {
getTemplate,
listTemplates,
matchBlocks,
parseMarkdown,
parseMarkdownWithTemplate,
wasmReady
wasmReady,
type ParseResultSuccess
} from '../wasm';
import EditorPanel from './EditorPanel.svelte';
let templates = $state([] as string[]);
let templateValue = $state('');
let markdownValue = $state(`---
const DEFAULT_MARKDOWN_VALUE = `---
_type: Recipe
author.name: Max Richter
---
@@ -28,20 +30,45 @@ My favourite baguette recipe
## Steps
1. Mix Flour Water and Salt
2. Bake the bread`);
2. Bake the bread`;
const DEFAULT_TEMPLATE_VALUE = '';
let templateValue = $state(
typeof window !== 'undefined'
? localStorage.getItem('templateValue') || DEFAULT_TEMPLATE_VALUE
: DEFAULT_TEMPLATE_VALUE
);
let markdownValue = $state(
typeof window !== 'undefined'
? localStorage.getItem('markdownValue') || DEFAULT_MARKDOWN_VALUE
: DEFAULT_MARKDOWN_VALUE
);
let jsonOutput = $state('');
let detectedSchemaName = $derived.by(() => {
try {
const parsed = JSON.parse(jsonOutput);
return parsed['_schema'];
} catch (err) {
return JSON.parse(jsonOutput)['_schema'];
} catch {
return undefined;
}
});
let timings = $state<ParseResult['timings'] | null>(null);
let status = $state<'success' | 'error' | undefined>(undefined);
let timings = $state<ParseResultSuccess['timings'] | null>(null);
let templateStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined);
let dataStatus = $state<'success' | 'error' | 'indeterminate' | undefined>(undefined);
$effect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('templateValue', templateValue);
}
});
$effect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('markdownValue', markdownValue);
}
});
$effect(() => {
if ($wasmReady) {
@@ -54,20 +81,40 @@ My favourite baguette recipe
if (!$wasmReady) {
jsonOutput = 'Loading wasm...';
timings = null;
status = undefined;
templateStatus = undefined;
dataStatus = undefined;
return;
}
try {
const result = templateValue
? parseMarkdownWithTemplate(markdownValue, templateValue)
: parseMarkdown(markdownValue);
jsonOutput = JSON.stringify(result.data, null, 2);
timings = result.timings;
status = 'success';
if ('error' in result) {
jsonOutput = result.error;
if (result.error.startsWith('failed to compile template')) {
templateStatus = 'error';
dataStatus = 'indeterminate';
} else {
templateStatus = undefined;
dataStatus = 'error';
}
} else {
jsonOutput = JSON.stringify(result.data, null, 2);
timings = result.timings;
templateStatus = 'success';
dataStatus = 'success';
}
} catch (e: unknown) {
jsonOutput = (e as Error).message;
timings = null;
status = 'error';
if (jsonOutput.startsWith('failed to compile template')) {
templateStatus = 'error';
dataStatus = 'indeterminate';
} else {
templateStatus = undefined;
dataStatus = 'error';
}
}
});
@@ -79,47 +126,64 @@ My favourite baguette recipe
console.error(e);
}
}
function resetMarkdown() {
markdownValue = DEFAULT_MARKDOWN_VALUE;
}
</script>
<div class="grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-3">
<EditorPanel
title="Template"
bind:value={templateValue}
placeholder="Enter your Marka template here..."
{status}
timing={timings?.template_compilation}
subtitle="Define your mapping schema"
>
{#snippet headerActions()}
<select
onchange={(e) => loadTemplate(e.currentTarget.value)}
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
<option value="">Load a template</option>
{#each templates as template (template)}
<option value={template}>{template}</option>
{/each}
</select>
{/snippet}
</EditorPanel>
<div class="flex flex-1 overflow-hidden">
<div class="grid flex-1 grid-cols-1 overflow-hidden lg:grid-cols-3">
<EditorPanel
title="Template"
bind:value={templateValue}
placeholder="Enter your Marka template here..."
status={templateStatus}
timing={timings?.template_compilation}
subtitle="Define your mapping schema"
langExtension={markdown()}
>
{#snippet headerActions()}
<select
onchange={(e) => loadTemplate(e.currentTarget.value)}
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
<option value="">Load a template</option>
{#each templates as template (template)}
<option value={template}>{template}</option>
{/each}
</select>
{/snippet}
</EditorPanel>
<EditorPanel
title="Markdown"
bind:value={markdownValue}
placeholder="Enter your markdown content here..."
{status}
timing={timings?.markdown_parsing}
subtitle="Your source content"
/>
<EditorPanel
title="Markdown"
bind:value={markdownValue}
placeholder="Enter your markdown content here..."
timing={timings?.markdown_parsing}
subtitle="Your source content"
langExtension={markdown()}
>
{#snippet headerActions()}
<button
onclick={resetMarkdown}
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
>
Reset
</button>
{/snippet}
</EditorPanel>
<EditorPanel
title="Data"
value={jsonOutput}
readonly={true}
{status}
subtitle="Parsed JSON output"
pillText={!templateValue && detectedSchemaName
? `Detected Template: ${detectedSchemaName}`
: undefined}
/>
<EditorPanel
title="Data"
value={jsonOutput}
readonly={true}
status={dataStatus}
subtitle="Parsed JSON output"
pillText={!templateValue && detectedSchemaName
? `Detected Template: ${detectedSchemaName}`
: undefined}
langExtension={json()}
/>
</div>
</div>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
let { class: className = '' } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>

View File

@@ -0,0 +1,10 @@
<img src="/logo.svg" alt="logo" width="100%" />
<style>
img {
height: 64px;
width: 64px;
aspect-ratio: 1;
filter: drop-shadow(0px 0px 8px #0002);
}
</style>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
let { class: className = '' } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="M18 12H6" />
</svg>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
let { class: className = '' } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>

View File

@@ -1,74 +1,99 @@
import { readable } from 'svelte/store';
import { readable } from "svelte/store";
declare global {
interface Window {
Go: {
new (): {
run: (inst: WebAssembly.Instance) => Promise<void>;
importObject: WebAssembly.Imports;
};
};
markaParseFile: (input: string) => string;
markaParseFileWithTemplate: (markdown: string, template: string) => string;
markaListTemplates: () => string;
markaGetTemplate: (name: string) => string;
}
interface Window {
Go: {
new(): {
run: (inst: WebAssembly.Instance) => Promise<void>;
importObject: WebAssembly.Imports;
};
};
markaMatchBlocks: (input: string) => unknown;
markaParseFile: (input: string) => string;
markaParseFileWithTemplate: (markdown: string, template: string) => string;
markaListTemplates: () => string;
markaGetTemplate: (name: string) => string;
}
}
export const wasmReady = readable(false, (set) => {
if (typeof window === 'undefined') {
return;
}
if (typeof window === "undefined") {
return;
}
const loadWasm = async () => {
const go = new window.Go();
try {
const result = await WebAssembly.instantiateStreaming(fetch('/main.wasm'), go.importObject);
go.run(result.instance);
set(true);
} catch (error) {
console.error('Error loading wasm module:', error);
}
};
const loadWasm = async () => {
const go = new window.Go();
try {
const result = await WebAssembly.instantiateStreaming(
fetch("/main.wasm"),
go.importObject,
);
go.run(result.instance);
set(true);
} catch (error) {
console.error("Error loading wasm module:", error);
}
};
if (document.readyState === 'complete') {
loadWasm();
} else {
window.addEventListener('load', loadWasm);
}
if (document.readyState === "complete") {
loadWasm();
} else {
window.addEventListener("load", loadWasm);
}
});
export interface ParseResult {
data: unknown;
timings: { [key: string]: number };
}
export type ParseResultSuccess = {
data: unknown;
timings: { [key: string]: number };
};
export type ParseResultError = {
error: string;
};
export type ParseResult = ParseResultSuccess | ParseResultError;
export function parseMarkdown(markdown: string): ParseResult {
if (typeof window.markaParseFile !== 'function') {
throw new Error('Wasm module not ready');
}
const result = window.markaParseFile(markdown);
return JSON.parse(result);
if (typeof window.markaParseFile !== "function") {
throw new Error("Wasm module not ready");
}
const result = window.markaParseFile(markdown);
if (result.error) return result;
return JSON.parse(result);
}
export function parseMarkdownWithTemplate(markdown: string, template: string): ParseResult {
if (typeof window.markaParseFileWithTemplate !== 'function') {
throw new Error('Wasm module not ready');
}
const result = window.markaParseFileWithTemplate(markdown, template);
return JSON.parse(result);
export function matchBlocks(markdown: string): ParseResult {
if (typeof window.markaMatchBlocks !== "function") {
throw new Error("Wasm module not ready");
}
const result = window.markaMatchBlocks(markdown) as ParseResult;
if (result.error) return result;
return JSON.parse(result);
}
export function parseMarkdownWithTemplate(
markdown: string,
template: string,
): ParseResult {
if (typeof window.markaParseFileWithTemplate !== "function") {
throw new Error("Wasm module not ready");
}
const result = window.markaParseFileWithTemplate(markdown, template);
if (result.error) return result;
return JSON.parse(result);
}
export function listTemplates(): string[] {
if (typeof window.markaListTemplates !== 'function') {
throw new Error('Wasm module not ready');
}
const result = window.markaListTemplates();
return JSON.parse(result);
if (typeof window.markaListTemplates !== "function") {
throw new Error("Wasm module not ready");
}
const result = window.markaListTemplates();
return JSON.parse(result);
}
export function getTemplate(name: string): string {
if (typeof window.markaGetTemplate !== 'function') {
throw new Error('Wasm module not ready');
}
return window.markaGetTemplate(name);
if (typeof window.markaGetTemplate !== "function") {
throw new Error("Wasm module not ready");
}
return window.markaGetTemplate(name);
}

View File

@@ -1,12 +1,13 @@
<script lang="ts">
import '../app.css';
import favicon from '$lib/assets/favicon.svg';
import '../app.css';
let { children } = $props();
</script>
<svelte:head>
<link rel="icon" href={favicon} />
<title>Marka Playground</title>
<script src="/wasm_exec.js"></script>
</svelte:head>

View File

@@ -5,7 +5,9 @@
<div class="bg-background text-foreground flex min-h-screen flex-col">
<Header />
<Playground />
<div class="flex flex-1 overflow-hidden">
<Playground />
</div>
</div>
<style>

View File

@@ -0,0 +1,15 @@
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 50.0001C0 28.9476 19.9406 13.3665 43.3168 13.3665C52.852 13.3665 71.2871 17.0793 79.703 30.1981C79.703 30.1981 75.9901 41.3368 62.8713 43.5645C49.7525 45.7922 40.5941 30.4457 30.9406 30.4457C21.2871 30.4457 13.3663 38.7833 13.3663 51.9283C13.3663 59.1669 15.6639 65.9462 19.6134 71.7442C27.299 83.0278 41.2416 90.5942 56.6832 90.5942C80.0594 90.5942 100 71.3259 100 50.0001C100 77.6145 77.6144 100 50 100C22.3856 100 0 77.6145 0 50.0001Z" fill="url(#paint0_linear_12_27)"/>
<path d="M86.6337 51.9282C86.6337 60.2351 82.1782 70.1022 72.7723 74.505C63.3663 78.9077 57.4257 68.0693 43.3168 64.1089C29.2079 60.1485 19.6134 71.7441 19.6134 71.7441C27.299 83.0277 41.2416 90.5941 56.6832 90.5941C80.0594 90.5941 100 71.3257 100 50C100 22.3856 77.6144 0 50 0C22.3856 0 0 22.3856 0 50C0 28.9475 19.9406 13.3663 43.3168 13.3663C52.852 13.3663 61.9391 16.0842 69.3069 20.8153C80.0015 27.6822 87.0733 38.7896 86.6337 51.9802" fill="url(#paint1_linear_12_27)"/>
<defs>
<linearGradient id="paint0_linear_12_27" x1="75.1033" y1="92.9796" x2="22.9842" y2="7.71589" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#545454"/>
</linearGradient>
<linearGradient id="paint1_linear_12_27" x1="23.9405" y1="7.17252" x2="75.3423" y2="92.0288" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#545454"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,7 +1,14 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import tailwindcss from "@tailwindcss/vite";
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [tailwindcss(), sveltekit()]
plugins: [tailwindcss(), sveltekit()],
optimizeDeps: {
exclude: [
"svelte-codemirror-editor",
"codemirror",
"@codemirror/language-javascript", /* ... */
],
},
});

View File

@@ -14,13 +14,12 @@ func matchBlocks(_ js.Value, args []js.Value) any {
if len(args) == 0 {
return js.ValueOf(map[string]any{"error": "missing markdown"})
}
t, err := p.MatchBlocks(args[0].String())
t, err := p.MatchBlocks(args[0].String(), args[1].String())
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
}
jsonString,_ := json.Marshal(t)
jsonString, _ := json.Marshal(t)
return js.ValueOf(string(jsonString)) // plain string
}
@@ -44,7 +43,7 @@ func parseFile(_ js.Value, args []js.Value) any {
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
}
b, err := json.Marshal(res) // return JSON string to avoid reflect-heavy bridging
b, err := json.Marshal(res)
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
}
@@ -59,7 +58,7 @@ func parseFileWithTemplate(_ js.Value, args []js.Value) any {
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
}
b, err := json.Marshal(res) // return JSON string to avoid reflect-heavy bridging
b, err := json.Marshal(res)
if err != nil {
return js.ValueOf(map[string]any{"error": err.Error()})
}
@@ -98,4 +97,3 @@ func main() {
js.Global().Set("markaGetTemplate", js.FuncOf(getTemplate))
select {}
}