fix: most of the template blocks
This commit is contained in:
@@ -20,21 +20,27 @@ func ParseBlock(input string, block template.Block) (any, error) {
|
|||||||
case template.CodecHashtags:
|
case template.CodecHashtags:
|
||||||
return Keywords(input, block)
|
return Keywords(input, block)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unknown codec: %s", block.Codec)
|
fmt.Printf("%#v\n", block)
|
||||||
|
return nil, fmt.Errorf("unknown codec '%s'", block.Codec)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse(matches []matcher.Block) (any, error) {
|
func Parse(matches []matcher.Block) (any, error) {
|
||||||
var result any
|
var result any
|
||||||
|
|
||||||
for _, m := range matches {
|
for i, m := range matches {
|
||||||
if m.Block.Path == "@index" {
|
if m.Block.Path == "@index" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
input := m.GetContent()
|
input := m.GetContent()
|
||||||
value, err := ParseBlock(input, m.Block)
|
value, err := ParseBlock(input, m.Block)
|
||||||
|
var blockIdentifier any
|
||||||
|
blockIdentifier = m.Block.Path
|
||||||
|
if blockIdentifier == "" {
|
||||||
|
blockIdentifier = fmt.Sprintf("#%d", i)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse block(%s): %w", m.Block.Path, err)
|
return nil, fmt.Errorf("failed to parse block(%s) -> %w", blockIdentifier, err)
|
||||||
}
|
}
|
||||||
result = utils.SetPathValue(m.Block.Path, value, result)
|
result = utils.SetPathValue(m.Block.Path, value, result)
|
||||||
}
|
}
|
||||||
|
@@ -50,7 +50,6 @@ func MatchBlocksFuzzy(markdown string, templateBlocks []template.Block, maxDist
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the last block
|
|
||||||
if len(templateBlocks) > 0 {
|
if len(templateBlocks) > 0 {
|
||||||
lastBlock := templateBlocks[len(templateBlocks)-1]
|
lastBlock := templateBlocks[len(templateBlocks)-1]
|
||||||
if lastBlock.Type == template.DataBlock {
|
if lastBlock.Type == template.DataBlock {
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
"git.max-richter.dev/max/marka/testdata"
|
"git.max-richter.dev/max/marka/testdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFuzzyFindAll(t *testing.T) {
|
func TestMatch_FuzzyFindAll(t *testing.T) {
|
||||||
recipeMd := testdata.Read(t, "baguette/input.md")
|
recipeMd := testdata.Read(t, "baguette/input.md")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -36,13 +36,14 @@ func TestFuzzyFindAll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFuzzyBlockMatch(t *testing.T) {
|
func TestMatch_FuzzyBlockBaguette(t *testing.T) {
|
||||||
recipeMd := testdata.Read(t, "baguette/input.md")
|
recipeMd := testdata.Read(t, "baguette/input.md")
|
||||||
schemaMd, err := registry.GetTemplate("Recipe")
|
schemaMd, err := registry.GetTemplate("Recipe")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failed to load template: %s", err.Error())
|
t.Errorf("Failed to load template: %s", err.Error())
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
blocks, err := template.CompileTemplate(schemaMd)
|
blocks, err := template.CompileTemplate(schemaMd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failed to compile template: %s", err.Error())
|
t.Errorf("Failed to compile template: %s", err.Error())
|
||||||
@@ -51,9 +52,8 @@ func TestFuzzyBlockMatch(t *testing.T) {
|
|||||||
|
|
||||||
matches := matcher.MatchBlocksFuzzy(string(recipeMd), blocks, 0.3)
|
matches := matcher.MatchBlocksFuzzy(string(recipeMd), blocks, 0.3)
|
||||||
|
|
||||||
for _, b := range blocks {
|
for _, m := range matches {
|
||||||
fmt.Printf("Block: %+v\n", b)
|
fmt.Printf("Content: '%s'->'%q'\n\n", m.Block.Path, m.GetContent())
|
||||||
fmt.Printf("Content: '%q'\n\n", b.GetContent())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expected := []struct {
|
expected := []struct {
|
||||||
@@ -66,10 +66,7 @@ func TestFuzzyBlockMatch(t *testing.T) {
|
|||||||
value: "Baguette",
|
value: "Baguette",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "\nMy favourite baguette recipe",
|
value: "My favourite baguette recipe",
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "- Flour\n- Water\n- Salt",
|
value: "- Flour\n- Water\n- Salt",
|
||||||
@@ -87,11 +84,10 @@ func TestFuzzyBlockMatch(t *testing.T) {
|
|||||||
if expected[i].value != m.GetContent() {
|
if expected[i].value != m.GetContent() {
|
||||||
t.Errorf("Match %d did not match expected: %q", i, expected[i].value)
|
t.Errorf("Match %d did not match expected: %q", i, expected[i].value)
|
||||||
}
|
}
|
||||||
fmt.Printf("match: %s->%q\n", m.Block.Path, m.GetContent())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFuzzyBlockMatchSalad(t *testing.T) {
|
func TestMatch_FuzzyBlockSalad(t *testing.T) {
|
||||||
recipeMd := testdata.Read(t, "recipe_salad/input.md")
|
recipeMd := testdata.Read(t, "recipe_salad/input.md")
|
||||||
schemaMd, err := registry.GetTemplate("Recipe")
|
schemaMd, err := registry.GetTemplate("Recipe")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -115,9 +111,6 @@ func TestFuzzyBlockMatchSalad(t *testing.T) {
|
|||||||
{
|
{
|
||||||
value: "Simple Salad",
|
value: "Simple Salad",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: "#healthy #salad",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: "A quick green salad.",
|
value: "A quick green salad.",
|
||||||
},
|
},
|
||||||
|
@@ -5,7 +5,6 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.max-richter.dev/max/marka/parser/decoders"
|
"git.max-richter.dev/max/marka/parser/decoders"
|
||||||
"git.max-richter.dev/max/marka/parser/matcher"
|
"git.max-richter.dev/max/marka/parser/matcher"
|
||||||
@@ -53,79 +52,33 @@ func MatchBlocks(markdownContent, templateContent string) ([]matcher.Block, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ParseFile(markdownContent string) (any, error) {
|
func ParseFile(markdownContent string) (any, error) {
|
||||||
timings := make(map[string]int64)
|
|
||||||
|
|
||||||
startDetectType := time.Now()
|
|
||||||
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
|
||||||
|
|
||||||
contentType, err := DetectType(markdownContent)
|
contentType, err := DetectType(markdownContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not detect type -> %w", err)
|
return nil, fmt.Errorf("could not detect type -> %w", err)
|
||||||
}
|
}
|
||||||
timings["detect_type"] = time.Since(startDetectType).Milliseconds()
|
|
||||||
|
|
||||||
startGetTemplate := time.Now()
|
|
||||||
templateContent, err := registry.GetTemplate(contentType)
|
templateContent, err := registry.GetTemplate(contentType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not get schema -> %w", err)
|
return nil, fmt.Errorf("could not get template -> %w", err)
|
||||||
}
|
|
||||||
timings["get_template"] = time.Since(startGetTemplate).Milliseconds()
|
|
||||||
|
|
||||||
startTemplate := time.Now()
|
|
||||||
tpl, err := template.CompileTemplate(templateContent)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to compile template -> %w", err)
|
|
||||||
}
|
|
||||||
timings["template_compilation"] = time.Since(startTemplate).Milliseconds()
|
|
||||||
|
|
||||||
startMarkdown := time.Now()
|
|
||||||
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
|
|
||||||
|
|
||||||
fmt.Println("Blocks: ", len(blocks))
|
|
||||||
for i, b := range blocks {
|
|
||||||
fmt.Printf("Block %d %+v\n", i, b)
|
|
||||||
fmt.Printf("Content %d: %q\n\n", i, b.GetContent())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := decoders.Parse(blocks)
|
return ParseFileWithTemplate(markdownContent, templateContent)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse blocks -> %w", err)
|
|
||||||
}
|
|
||||||
timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds()
|
|
||||||
|
|
||||||
response := map[string]any{
|
|
||||||
"data": result,
|
|
||||||
"timings": timings,
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseFileWithTemplate(markdownContent string, templateContent string) (any, error) {
|
func ParseFileWithTemplate(markdownContent string, templateContent string) (any, error) {
|
||||||
timings := make(map[string]int64)
|
|
||||||
|
|
||||||
startTemplate := time.Now()
|
|
||||||
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
markdownContent = strings.TrimSuffix(markdownContent, "\n")
|
||||||
|
|
||||||
tpl, err := template.CompileTemplate(templateContent)
|
tpl, err := template.CompileTemplate(templateContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to compile template -> %w", err)
|
return nil, fmt.Errorf("failed to compile template -> %w", err)
|
||||||
}
|
}
|
||||||
timings["template_compilation"] = time.Since(startTemplate).Milliseconds()
|
|
||||||
|
|
||||||
startMarkdown := time.Now()
|
|
||||||
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
|
blocks := matcher.MatchBlocksFuzzy(markdownContent, tpl, 0.3)
|
||||||
|
|
||||||
result, err := decoders.Parse(blocks)
|
result, err := decoders.Parse(blocks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse blocks -> %w", err)
|
return nil, fmt.Errorf("failed to compile blocks -> %w", err)
|
||||||
}
|
|
||||||
timings["markdown_parsing"] = time.Since(startMarkdown).Milliseconds()
|
|
||||||
|
|
||||||
response := map[string]any{
|
|
||||||
"data": result,
|
|
||||||
"timings": timings,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,30 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseRecipe_Golden(t *testing.T) {
|
func TestParse_DetectType(t *testing.T) {
|
||||||
|
recipe := testdata.Read(t, "recipe_salad/input.md")
|
||||||
|
article := testdata.Read(t, "article_simple/input.md")
|
||||||
|
|
||||||
|
recipeType, err := parser.DetectType(string(recipe))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to detect recipeType: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
articleType, err := parser.DetectType(string(article))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to detect articleType: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipeType != "Recipe" {
|
||||||
|
t.Errorf("recipeType did not match expected type 'Recipe' -> %s", recipeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if articleType != "Article" {
|
||||||
|
t.Errorf("articleType did not match expected type 'Article' -> %s", articleType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse_RecipeSalad(t *testing.T) {
|
||||||
inputContent := testdata.Read(t, "recipe_salad/input.md")
|
inputContent := testdata.Read(t, "recipe_salad/input.md")
|
||||||
output := testdata.Read(t, "recipe_salad/output.json")
|
output := testdata.Read(t, "recipe_salad/output.json")
|
||||||
|
|
||||||
@@ -18,19 +41,17 @@ func TestParseRecipe_Golden(t *testing.T) {
|
|||||||
t.Fatalf("ParseFile: %v", err)
|
t.Fatalf("ParseFile: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gotMap := got.(map[string]any)
|
|
||||||
|
|
||||||
var want map[string]any
|
var want map[string]any
|
||||||
if err := json.Unmarshal(output, &want); err != nil {
|
if err := json.Unmarshal(output, &want); err != nil {
|
||||||
t.Fatalf("unmarshal expected.json: %v", err)
|
t.Fatalf("unmarshal expected.json: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if diff := cmp.Diff(want, gotMap["data"]); diff != "" {
|
if diff := cmp.Diff(want, got); diff != "" {
|
||||||
t.Fatalf("JSON mismatch (-want +got):\n%s", diff)
|
t.Fatalf("JSON mismatch (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseRecipe_NoDescription(t *testing.T) {
|
func TestParse_RecipeNoDescription(t *testing.T) {
|
||||||
inputContent := testdata.Read(t, "recipe_no_description/input.md")
|
inputContent := testdata.Read(t, "recipe_no_description/input.md")
|
||||||
|
|
||||||
got, err := parser.ParseFile(string(inputContent))
|
got, err := parser.ParseFile(string(inputContent))
|
||||||
@@ -49,7 +70,7 @@ func TestParseRecipe_NoDescription(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseRecipe_Baguette(t *testing.T) {
|
func TestParse_Baguette(t *testing.T) {
|
||||||
inputContent := testdata.Read(t, "baguette/input.md")
|
inputContent := testdata.Read(t, "baguette/input.md")
|
||||||
|
|
||||||
got, err := parser.ParseFile(string(inputContent))
|
got, err := parser.ParseFile(string(inputContent))
|
||||||
@@ -68,7 +89,7 @@ func TestParseRecipe_Baguette(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArticle_Simple(t *testing.T) {
|
func TestParse_Article(t *testing.T) {
|
||||||
inputContent := testdata.Read(t, "article_simple/input.md")
|
inputContent := testdata.Read(t, "article_simple/input.md")
|
||||||
|
|
||||||
got, err := parser.ParseFile(string(inputContent))
|
got, err := parser.ParseFile(string(inputContent))
|
||||||
|
19
playground/src/app.d.ts
vendored
19
playground/src/app.d.ts
vendored
@@ -8,6 +8,23 @@ declare global {
|
|||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Go {
|
||||||
|
new(): {
|
||||||
|
run: (inst: WebAssembly.Instance) => Promise<void>;
|
||||||
|
importObject: WebAssembly.Imports;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const marka: {
|
||||||
|
matchBlocks(s: string, t: string): string;
|
||||||
|
detectType(markdown: string): string;
|
||||||
|
parseFile(input: string): string;
|
||||||
|
parseFileWithTemplate(markdown: string, template: string): string;
|
||||||
|
listTemplates(): string;
|
||||||
|
getTemplate(name: string): string;
|
||||||
|
compileTemplate(source: string): string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
@@ -2,6 +2,7 @@
|
|||||||
import { json } from '@codemirror/lang-json';
|
import { json } from '@codemirror/lang-json';
|
||||||
import { markdown } from '@codemirror/lang-markdown';
|
import { markdown } from '@codemirror/lang-markdown';
|
||||||
import {
|
import {
|
||||||
|
compileTemplate,
|
||||||
getTemplate,
|
getTemplate,
|
||||||
listTemplates,
|
listTemplates,
|
||||||
parseMarkdown,
|
parseMarkdown,
|
||||||
@@ -86,9 +87,12 @@ My favourite baguette recipe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
compileTemplate(templateValue);
|
||||||
|
|
||||||
const result = templateValue
|
const result = templateValue
|
||||||
? parseMarkdownWithTemplate(markdownValue, templateValue)
|
? parseMarkdownWithTemplate(markdownValue, templateValue)
|
||||||
: parseMarkdown(markdownValue);
|
: parseMarkdown(markdownValue);
|
||||||
|
console.log({ result });
|
||||||
|
|
||||||
if ('error' in result) {
|
if ('error' in result) {
|
||||||
jsonOutput = '';
|
jsonOutput = '';
|
||||||
|
@@ -1,20 +1,6 @@
|
|||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
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) => {
|
export const wasmReady = readable(false, (set) => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
@@ -22,7 +8,7 @@ export const wasmReady = readable(false, (set) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadWasm = async () => {
|
const loadWasm = async () => {
|
||||||
const go = new window.Go();
|
const go = new globalThis.Go();
|
||||||
try {
|
try {
|
||||||
const result = await WebAssembly.instantiateStreaming(
|
const result = await WebAssembly.instantiateStreaming(
|
||||||
fetch("/main.wasm"),
|
fetch("/main.wasm"),
|
||||||
@@ -38,7 +24,7 @@ export const wasmReady = readable(false, (set) => {
|
|||||||
if (document.readyState === "complete") {
|
if (document.readyState === "complete") {
|
||||||
loadWasm();
|
loadWasm();
|
||||||
} else {
|
} else {
|
||||||
window.addEventListener("load", loadWasm);
|
globalThis.addEventListener("load", loadWasm);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,46 +40,75 @@ export type ParseResultError = {
|
|||||||
export type ParseResult = ParseResultSuccess | ParseResultError;
|
export type ParseResult = ParseResultSuccess | ParseResultError;
|
||||||
|
|
||||||
export function parseMarkdown(markdown: string): ParseResult {
|
export function parseMarkdown(markdown: string): ParseResult {
|
||||||
if (typeof window.markaParseFile !== "function") {
|
if (typeof globalThis.marka?.parseFile !== "function") {
|
||||||
throw new Error("Wasm module not ready");
|
throw new Error("Wasm module not ready");
|
||||||
}
|
}
|
||||||
const result = window.markaParseFile(markdown);
|
const resultString = globalThis.marka.parseFile(markdown);
|
||||||
if (result.error) return result;
|
return JSON.parse(resultString);
|
||||||
return JSON.parse(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function matchBlocks(markdown: string): ParseResult {
|
export function compileTemplate(templateSource: string) {
|
||||||
if (typeof window.markaMatchBlocks !== "function") {
|
if (typeof globalThis.marka?.compileTemplate !== "function") {
|
||||||
throw new Error("Wasm module not ready");
|
throw new Error("Wasm module not ready");
|
||||||
}
|
}
|
||||||
const result = window.markaMatchBlocks(markdown) as ParseResult;
|
const resultString = globalThis.marka.compileTemplate(templateSource);
|
||||||
if (result.error) return result;
|
const result = JSON.parse(resultString);
|
||||||
return JSON.parse(result);
|
console.log({ result });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchBlocks(markdown: string, template: string): ParseResult {
|
||||||
|
if (typeof globalThis.marka?.matchBlocks !== "function") {
|
||||||
|
throw new Error("Wasm module not ready");
|
||||||
|
}
|
||||||
|
const resultString = globalThis.marka.matchBlocks(markdown, template);
|
||||||
|
return JSON.parse(resultString);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseMarkdownWithTemplate(
|
export function parseMarkdownWithTemplate(
|
||||||
markdown: string,
|
markdown: string,
|
||||||
template: string,
|
template: string,
|
||||||
): ParseResult {
|
): ParseResult {
|
||||||
if (typeof window.markaParseFileWithTemplate !== "function") {
|
if (typeof globalThis.marka?.parseFileWithTemplate !== "function") {
|
||||||
throw new Error("Wasm module not ready");
|
throw new Error("Wasm module not ready");
|
||||||
}
|
}
|
||||||
const result = window.markaParseFileWithTemplate(markdown, template);
|
const resultString = globalThis.marka.parseFileWithTemplate(
|
||||||
if (result.error) return result;
|
markdown,
|
||||||
return JSON.parse(result);
|
template,
|
||||||
|
);
|
||||||
|
return JSON.parse(resultString);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listTemplates(): string[] {
|
export function listTemplates(): string[] {
|
||||||
if (typeof window.markaListTemplates !== "function") {
|
if (typeof globalThis.marka?.listTemplates !== "function") {
|
||||||
throw new Error("Wasm module not ready");
|
throw new Error("Wasm module not ready");
|
||||||
}
|
}
|
||||||
const result = window.markaListTemplates();
|
const resultString = globalThis.marka.listTemplates();
|
||||||
return JSON.parse(result);
|
return JSON.parse(resultString);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTemplate(name: string): string {
|
export function getTemplate(name: string): string {
|
||||||
if (typeof window.markaGetTemplate !== "function") {
|
if (typeof globalThis.marka?.getTemplate !== "function") {
|
||||||
throw new Error("Wasm module not ready");
|
throw new Error("Wasm module not ready");
|
||||||
}
|
}
|
||||||
return window.markaGetTemplate(name);
|
return globalThis.marka.getTemplate(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function detectType(markdown: string): string | ParseResultError {
|
||||||
|
if (typeof globalThis.marka?.detectType !== "function") {
|
||||||
|
throw new Error("Wasm module not ready");
|
||||||
|
}
|
||||||
|
const result = globalThis.marka.detectType(markdown);
|
||||||
|
try {
|
||||||
|
// If the result is a JSON string with an error, parse and return it
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
if (parsed.error) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Otherwise, it's a plain string for success
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
@@ -7,12 +7,13 @@ OUT_WASM="$OUT_DIR/main.wasm"
|
|||||||
|
|
||||||
mkdir -p "$OUT_DIR"
|
mkdir -p "$OUT_DIR"
|
||||||
|
|
||||||
tinygo build -target=wasm -opt=z -no-debug -panic=trap -gc=leaking \
|
tinygo build -target=wasm \
|
||||||
|
-opt=z -no-debug -panic=print -gc=leaking \
|
||||||
-o "$OUT_WASM" "$SCRIPT_DIR"
|
-o "$OUT_WASM" "$SCRIPT_DIR"
|
||||||
|
|
||||||
# Optional post-process (run only if tools exist)
|
# Optional post-process (run only if tools exist)
|
||||||
# command -v wasm-opt >/dev/null && wasm-opt -Oz --strip-debug --strip-dwarf --strip-producers \
|
command -v wasm-opt >/dev/null && wasm-opt -Oz --strip-debug --strip-dwarf --strip-producers \
|
||||||
# -o "$OUT_WASM.tmp" "$OUT_WASM" && mv "$OUT_WASM.tmp" "$OUT_WASM"
|
-o "$OUT_WASM.tmp" "$OUT_WASM" && mv "$OUT_WASM.tmp" "$OUT_WASM"
|
||||||
# command -v wasm-strip >/dev/null && wasm-strip "$OUT_WASM"
|
# command -v wasm-strip >/dev/null && wasm-strip "$OUT_WASM"
|
||||||
# command -v brotli >/dev/null && brotli -f -q 11 "$OUT_WASM" -o "$OUT_WASM.br"
|
# command -v brotli >/dev/null && brotli -f -q 11 "$OUT_WASM" -o "$OUT_WASM.br"
|
||||||
# command -v gzip >/dev/null && gzip -c -9 "$OUT_WASM" > "$OUT_WASM.gz"
|
# command -v gzip >/dev/null && gzip -c -9 "$OUT_WASM" > "$OUT_WASM.gz"
|
||||||
|
@@ -8,92 +8,108 @@ import (
|
|||||||
|
|
||||||
p "git.max-richter.dev/max/marka/parser"
|
p "git.max-richter.dev/max/marka/parser"
|
||||||
"git.max-richter.dev/max/marka/registry"
|
"git.max-richter.dev/max/marka/registry"
|
||||||
|
"git.max-richter.dev/max/marka/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
func matchBlocks(_ js.Value, args []js.Value) any {
|
func wrapError(err error) string {
|
||||||
if len(args) == 0 {
|
errMap := map[string]any{"error": err.Error()}
|
||||||
return js.ValueOf(map[string]any{"error": "missing markdown"})
|
errJSON, _ := json.Marshal(errMap)
|
||||||
}
|
return string(errJSON)
|
||||||
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)
|
|
||||||
|
|
||||||
return js.ValueOf(string(jsonString)) // plain string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectType(_ js.Value, args []js.Value) any {
|
func MatchBlocks(this js.Value, args []js.Value) any {
|
||||||
if len(args) == 0 {
|
s := args[0].String()
|
||||||
return js.ValueOf(map[string]any{"error": "missing markdown"})
|
t := args[1].String()
|
||||||
}
|
matched, err := p.MatchBlocks(s, t)
|
||||||
t, err := p.DetectType(args[0].String())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
return wrapError(err)
|
||||||
}
|
}
|
||||||
return js.ValueOf(t) // plain string
|
jsonString, _ := json.Marshal(matched)
|
||||||
|
return string(jsonString)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFile(_ js.Value, args []js.Value) any {
|
func DetectType(this js.Value, args []js.Value) any {
|
||||||
if len(args) == 0 {
|
markdown := args[0].String()
|
||||||
return js.ValueOf(map[string]any{"error": "missing markdown"})
|
t, err := p.DetectType(markdown)
|
||||||
}
|
|
||||||
res, err := p.ParseFile(args[0].String())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
return wrapError(err)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseFile(this js.Value, args []js.Value) any {
|
||||||
|
markdown := args[0].String()
|
||||||
|
res, err := p.ParseFile(markdown)
|
||||||
|
if err != nil {
|
||||||
|
return wrapError(err)
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(res)
|
b, err := json.Marshal(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
return wrapError(err)
|
||||||
}
|
}
|
||||||
return js.ValueOf(string(b))
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFileWithTemplate(_ js.Value, args []js.Value) any {
|
func ParseFileWithTemplate(this js.Value, args []js.Value) any {
|
||||||
if len(args) < 2 {
|
markdown := args[0].String()
|
||||||
return js.ValueOf(map[string]any{"error": "missing markdown or template"})
|
template := args[1].String()
|
||||||
}
|
res, err := p.ParseFileWithTemplate(markdown, template)
|
||||||
res, err := p.ParseFileWithTemplate(args[0].String(), args[1].String())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
return wrapError(err)
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(res)
|
b, err := json.Marshal(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
return wrapError(err)
|
||||||
}
|
}
|
||||||
return js.ValueOf(string(b))
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listTemplates(_ js.Value, args []js.Value) any {
|
func ListTemplates(this js.Value, args []js.Value) any {
|
||||||
templates, err := registry.ListTemplates()
|
templates, err := registry.ListTemplates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
return wrapError(err)
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(templates)
|
b, err := json.Marshal(templates)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
return wrapError(err)
|
||||||
}
|
}
|
||||||
return js.ValueOf(string(b))
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTemplate(_ js.Value, args []js.Value) any {
|
func GetTemplate(this js.Value, args []js.Value) any {
|
||||||
if len(args) == 0 {
|
name := args[0].String()
|
||||||
return js.ValueOf(map[string]any{"error": "missing template name"})
|
template, err := registry.GetTemplate(name)
|
||||||
}
|
|
||||||
template, err := registry.GetTemplate(args[0].String())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return js.ValueOf(map[string]any{"error": err.Error()})
|
return wrapError(err)
|
||||||
}
|
}
|
||||||
return js.ValueOf(template)
|
return template
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompileTemplate(this js.Value, args []js.Value) any {
|
||||||
|
source := args[0].String()
|
||||||
|
template, err := template.CompileTemplate(source)
|
||||||
|
if err != nil {
|
||||||
|
return wrapError(err)
|
||||||
|
}
|
||||||
|
b, err := json.Marshal(template)
|
||||||
|
if err != nil {
|
||||||
|
return wrapError(err)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
js.Global().Set("markaDetectType", js.FuncOf(detectType))
|
marka := js.Global().Get("Object").New()
|
||||||
js.Global().Set("markaParseFile", js.FuncOf(parseFile))
|
|
||||||
js.Global().Set("markaParseFileWithTemplate", js.FuncOf(parseFileWithTemplate))
|
marka.Set("matchBlocks", js.FuncOf(MatchBlocks))
|
||||||
js.Global().Set("markaMatchBlocks", js.FuncOf(matchBlocks))
|
marka.Set("detectType", js.FuncOf(DetectType))
|
||||||
js.Global().Set("markaListTemplates", js.FuncOf(listTemplates))
|
marka.Set("parseFile", js.FuncOf(ParseFile))
|
||||||
js.Global().Set("markaGetTemplate", js.FuncOf(getTemplate))
|
marka.Set("parseFileWithTemplate", js.FuncOf(ParseFileWithTemplate))
|
||||||
|
marka.Set("listTemplates", js.FuncOf(ListTemplates))
|
||||||
|
marka.Set("getTemplate", js.FuncOf(GetTemplate))
|
||||||
|
marka.Set("compileTemplate", js.FuncOf(CompileTemplate))
|
||||||
|
|
||||||
|
js.Global().Set("marka", marka)
|
||||||
|
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
@@ -22,16 +22,13 @@
|
|||||||
pathAlias: rating
|
pathAlias: rating
|
||||||
- path: reviewRating.bestRating
|
- path: reviewRating.bestRating
|
||||||
codec: const
|
codec: const
|
||||||
value: 5
|
|
||||||
hidden: true
|
hidden: true
|
||||||
- path: reviewRating.worstRating
|
- path: reviewRating.worstRating
|
||||||
codec: const
|
codec: const
|
||||||
value: 1
|
|
||||||
hidden: true
|
hidden: true
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
# { headline }
|
# { headline }
|
||||||
{ keywords | hashtags }
|
|
||||||
|
|
||||||
{ articleBody }
|
{ articleBody }
|
||||||
|
@@ -10,7 +10,6 @@
|
|||||||
- path: "_type"
|
- path: "_type"
|
||||||
codec: const
|
codec: const
|
||||||
value: Recipe
|
value: Recipe
|
||||||
hidden: true
|
|
||||||
- path: image
|
- path: image
|
||||||
- path: author._type
|
- path: author._type
|
||||||
codec: const
|
codec: const
|
||||||
@@ -30,7 +29,6 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
# { name | text }
|
# { name | text }
|
||||||
{ keywords | hashtags,optional }
|
|
||||||
|
|
||||||
{ description | text }
|
{ description | text }
|
||||||
|
|
||||||
|
@@ -18,11 +18,11 @@ type yamlBlock struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type yamlField struct {
|
type yamlField struct {
|
||||||
Path string `yaml:"path"`
|
Path string `yaml:"path"`
|
||||||
Value any `yaml:"value,omitempty"`
|
Value any `yaml:"value,omitempty"`
|
||||||
Codec string `yaml:"codec"`
|
Codec string `yaml:"codec"`
|
||||||
Hidden bool `yaml:"hidden,omitempty"`
|
Hidden bool `yaml:"hidden,omitempty"`
|
||||||
PathAlias []string `yaml:"pathAlias,omitempty"`
|
PathAlias string `yaml:"pathAlias,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseYamlTemplate(input Slice) (block Block, err error) {
|
func parseYamlTemplate(input Slice) (block Block, err error) {
|
||||||
@@ -34,7 +34,7 @@ func parseYamlTemplate(input Slice) (block Block, err error) {
|
|||||||
dec.KnownFields(true)
|
dec.KnownFields(true)
|
||||||
|
|
||||||
if err := dec.Decode(&blk); err != nil {
|
if err := dec.Decode(&blk); err != nil {
|
||||||
return block, NewErrorf("content '%q' -> %w", cleaned, err).WithPosition(input.start, input.end)
|
return block, NewErrorf("failed to parse yaml -> %w", err).WithPosition(input.start, input.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
if blk.Path == "" {
|
if blk.Path == "" {
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
package template
|
package template
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
// CompileTemplate scans once, emitting:
|
// CompileTemplate scans once, emitting:
|
||||||
// - data blocks: inner content between a line that's exactly "{" and a line that's exactly "}"
|
// - data blocks: inner content between a line that's exactly "{" and a line that's exactly "}"
|
||||||
// - matching blocks: gaps between data blocks (excluding the brace lines themselves)
|
// - matching blocks: gaps between data blocks (excluding the brace lines themselves)
|
||||||
func CompileTemplate(templateSource string) ([]Block, error) {
|
func CompileTemplate(templateSource string) ([]Block, error) {
|
||||||
|
templateSource = strings.TrimSuffix(templateSource, "\n")
|
||||||
|
|
||||||
var out []Block
|
var out []Block
|
||||||
var curlyIndex int
|
var curlyIndex int
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package template_test
|
package template_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.max-richter.dev/max/marka/registry"
|
"git.max-richter.dev/max/marka/registry"
|
||||||
@@ -20,6 +21,10 @@ func TestExtractBlocks(t *testing.T) {
|
|||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i, b := range templateBlocks {
|
||||||
|
fmt.Printf("Block#%d: %q\n", i, b.GetContent())
|
||||||
|
}
|
||||||
|
|
||||||
expected := []template.Block{
|
expected := []template.Block{
|
||||||
{
|
{
|
||||||
Type: template.MatchingBlock,
|
Type: template.MatchingBlock,
|
||||||
@@ -58,14 +63,11 @@ func TestExtractBlocks(t *testing.T) {
|
|||||||
{Type: template.MatchingBlock},
|
{Type: template.MatchingBlock},
|
||||||
{Type: template.DataBlock, Path: "name", Codec: "text"},
|
{Type: template.DataBlock, Path: "name", Codec: "text"},
|
||||||
{Type: template.MatchingBlock},
|
{Type: template.MatchingBlock},
|
||||||
{Type: template.DataBlock, Path: "keywords", Codec: "hashtags", Optional: true},
|
|
||||||
{Type: template.MatchingBlock},
|
|
||||||
{Type: template.DataBlock, Path: "description", Codec: "text"},
|
{Type: template.DataBlock, Path: "description", Codec: "text"},
|
||||||
{Type: template.MatchingBlock},
|
{Type: template.MatchingBlock},
|
||||||
{Type: template.DataBlock, Path: "recipeIngredient", Codec: "list", ListTemplate: "- { . }"},
|
{Type: template.DataBlock, Path: "recipeIngredient", Codec: "list", ListTemplate: "- { . }"},
|
||||||
{Type: template.MatchingBlock},
|
{Type: template.MatchingBlock},
|
||||||
{Type: template.DataBlock, Path: "recipeInstructions", Codec: "list", ListTemplate: "{ @index }. { . }"},
|
{Type: template.DataBlock, Path: "recipeInstructions", Codec: "list", ListTemplate: "{ @index }. { . }"},
|
||||||
{Type: template.MatchingBlock},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(templateBlocks) != len(expected) {
|
if len(templateBlocks) != len(expected) {
|
||||||
|
1
testdata/data/article_simple/output.json
vendored
1
testdata/data/article_simple/output.json
vendored
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"_schema": "Article",
|
||||||
"_type": "Article",
|
"_type": "Article",
|
||||||
"headline": "My First Article",
|
"headline": "My First Article",
|
||||||
"author": {
|
"author": {
|
||||||
|
1
testdata/data/baguette/output.json
vendored
1
testdata/data/baguette/output.json
vendored
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"_schema": "Recipe",
|
||||||
"_type": "Recipe",
|
"_type": "Recipe",
|
||||||
"name": "Baguette",
|
"name": "Baguette",
|
||||||
"author": {
|
"author": {
|
||||||
|
1
testdata/data/recipe_salad/input.md
vendored
1
testdata/data/recipe_salad/input.md
vendored
@@ -8,7 +8,6 @@ recipeYield: 2 servings
|
|||||||
---
|
---
|
||||||
|
|
||||||
# Simple Salad
|
# Simple Salad
|
||||||
#healthy #salad
|
|
||||||
|
|
||||||
A quick green salad.
|
A quick green salad.
|
||||||
|
|
||||||
|
4
testdata/data/recipe_salad/output.json
vendored
4
testdata/data/recipe_salad/output.json
vendored
@@ -7,10 +7,6 @@
|
|||||||
"_type": "Person",
|
"_type": "Person",
|
||||||
"name": "Alex Chef"
|
"name": "Alex Chef"
|
||||||
},
|
},
|
||||||
"keywords": [
|
|
||||||
"healthy",
|
|
||||||
"salad"
|
|
||||||
],
|
|
||||||
"description": "A quick green salad.",
|
"description": "A quick green salad.",
|
||||||
"prepTime": "PT10M",
|
"prepTime": "PT10M",
|
||||||
"cookTime": "PT0M",
|
"cookTime": "PT0M",
|
||||||
|
Reference in New Issue
Block a user