Feature/add bi directional parsing #35

Merged
chris-kruining merged 3 commits from feature/add-bi-directional-parsing into main 2025-02-14 05:23:31 +00:00
8 changed files with 220 additions and 70 deletions
Showing only changes of commit 8e0eee5847 - Show all commits

View file

@ -1,9 +1,13 @@
import { defineConfig } from '@solidjs/start/config'; import { defineConfig } from '@solidjs/start/config';
import solidSvg from 'vite-plugin-solid-svg' import solidSvg from 'vite-plugin-solid-svg'
// import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({ export default defineConfig({
vite: { vite: {
resolve: {
alias: [
{ find: '@', replacement: 'F:\\Github\\calque\\node_modules\\' },
],
},
html: { html: {
cspNonce: 'KAAS_IS_AWESOME', cspNonce: 'KAAS_IS_AWESOME',
}, },
@ -12,65 +16,13 @@ export default defineConfig({
// }, // },
// }, // },
plugins: [ plugins: [
solidSvg() solidSvg(),
// VitePWA({ {
// strategies: 'injectManifest', name: 'temp',
// registerType: 'autoUpdate', configResolved(config) {
// injectRegister: false, console.log(config.resolve.alias);
},
// workbox: { }
// globPatterns: ['**/*.{js,css,html,svg,png,svg,ico}'],
// cleanupOutdatedCaches: true,
// clientsClaim: true,
// },
// injectManifest: {
// globPatterns: ['**/*.{js,css,html,svg,png,svg,ico}'],
// },
// manifest: {
// name: 'Calque - manage your i18n files',
// short_name: 'KAAS',
// description: 'Simple tool for maitaining i18n files',
// icons: [
// {
// src: '/images/favicon.dark.svg',
// type: 'image/svg+xml',
// sizes: 'any'
// }
// ],
// display_override: ['window-controls-overlay'],
// screenshots: [
// {
// src: '/images/screenshots/narrow.png',
// type: 'image/png',
// sizes: '538x1133',
// form_factor: 'narrow'
// },
// {
// src: '/images/screenshots/wide.png',
// type: 'image/png',
// sizes: '2092x1295',
// form_factor: 'wide'
// }
// ],
// file_handlers: [
// {
// action: '/edit',
// accept: {
// 'text/*': [
// '.json'
// ]
// }
// }
// ]
// },
// devOptions: {
// enabled: true,
// type: 'module',
// navigateFallback: 'index.html',
// },
// }),
], ],
}, },
solid: { solid: {

View file

@ -58,6 +58,13 @@ export function Textarea(props: TextareaProps) {
})) }))
}, 300); }, 300);
const onInput = (e: InputEvent) => {
const target = e.target as HTMLElement;
console.log(e);
console.log(target.innerText, target.textContent, target.innerHTML);
};
const onKeyUp = (e: KeyboardEvent) => { const onKeyUp = (e: KeyboardEvent) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -92,6 +99,7 @@ export function Textarea(props: TextareaProps) {
class={`${css.textarea} ${props.class}`} class={`${css.textarea} ${props.class}`}
lang={props.lang} lang={props.lang}
dir="auto" dir="auto"
oninput={onInput}
onkeyup={onKeyUp} onkeyup={onKeyUp}
on:keydown={e => e.stopPropagation()} on:keydown={e => e.stopPropagation()}
on:pointerdown={e => e.stopPropagation()} on:pointerdown={e => e.stopPropagation()}

View file

@ -0,0 +1,6 @@
export type { Source } from './source';
export { createParser as createHtmlParser } from './parser/html';
export { createParser as createMarkdownParser } from './parser/markdown';
export { createSource } from './source';

View file

@ -0,0 +1,28 @@
enum Decoration {
None = 0,
Bold = 1,
Italic = 2,
Underline = 4,
StrikeThrough = 8,
}
interface TextNode {
type: 'text';
decoration: Decoration;
nodes: (string | Node)[];
}
interface HeaderNode {
type: 'header';
nodes: Node[];
}
type Node = TextNode | HeaderNode;
export interface RichTextAST {
nodes: Node[];
}
export interface Parser {
parse(source: string): RichTextAST;
}

View file

@ -0,0 +1,9 @@
import { Parser } from "../parser";
export function createParser(): Parser {
return {
parse(value) {
return {};
},
};
}

View file

@ -0,0 +1,80 @@
import { Parser } from "../parser";
export function createParser(): Parser {
return {
parse(source) {
// console.log(source);
for (const token of tokenize(source)) {
console.log(token);
}
return {
nodes: [],
};
},
};
}
// const states = {
// none(): State {
// },
// } as const;
type Token = { start: number, length: number } & (
| { kind: 'bold' }
| { kind: 'italic' }
| { kind: 'underline' }
| { kind: 'strikethrough' }
| { kind: 'header', level: number }
| { kind: 'text', value: string }
);
function* tokenize(characters: string): Generator<Token, void, unknown> {
let buffer: string = '';
let clearBuffer = false;
let start = 0;
let i = 0;
for (const character of characters) {
if (buffer.length === 0) {
start = i;
}
buffer += character;
const length = buffer.length;
if (buffer === '**') {
yield { kind: 'bold', start, length };
clearBuffer = true;
}
else if (buffer === '') {
yield { kind: 'italic', start, length };
clearBuffer = true;
}
else if (buffer === ':') {
yield { kind: 'underline', start, length };
clearBuffer = true;
}
else if (buffer === ':') {
yield { kind: 'strikethrough', start, length };
clearBuffer = true;
}
else if (buffer.length > 1 && buffer.startsWith('#') && buffer.endsWith(' ')) {
yield { kind: 'header', start, length, level: buffer.length - 1 };
clearBuffer = true;
}
else if (buffer.length > 1 && buffer.startsWith('"') && buffer.endsWith('"')) {
yield { kind: 'text', start, length, value: buffer.slice(1, buffer.length - 1) };
clearBuffer = true;
}
if (clearBuffer) {
buffer = '';
clearBuffer = false;
}
i++;
}
}

View file

@ -0,0 +1,31 @@
import { createEffect, createSignal, Signal } from "solid-js";
import { Parser, RichTextAST } from "./parser";
export interface Source<TIn extends Parser, TOut extends Parser> {
readonly in: Signal<string>;
readonly out: Signal<string>;
}
export function createSource<TIn extends Parser, TOut extends Parser>(inParser: TIn, outParser: TOut, initalValue: string): Source<TIn, TOut> {
const [inValue, setIn] = createSignal<string>(initalValue);
const [outValue, setOut] = createSignal<string>('');
const [ast, setAst] = createSignal<RichTextAST>();
createEffect(() => {
setAst(inParser.parse(inValue()));
});
createEffect(() => {
setAst(outParser.parse(outValue()));
});
return {
get in() {
return [inValue, setIn] as Signal<string>;
},
get out() {
return [outValue, setOut] as Signal<string>;
},
};
}

View file

@ -15,6 +15,7 @@ import { writeClipboard } from "@solid-primitives/clipboard";
import { destructure } from "@solid-primitives/destructure"; import { destructure } from "@solid-primitives/destructure";
import css from "./edit.module.css"; import css from "./edit.module.css";
import { contentsOf } from "~/features/file/helpers"; import { contentsOf } from "~/features/file/helpers";
import { createHtmlParser, createMarkdownParser, createSource } from "~/features/source";
const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches; const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches;
@ -389,7 +390,41 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<Gr
const copyKey = createCommand('page.edit.command.copyKey', (key: string) => writeClipboard(key)); const copyKey = createCommand('page.edit.command.copyKey', (key: string) => writeClipboard(key));
return <Grid rows={rows()} locales={locales()} api={setApi}>{ const tempVal = `
# Header
this is **a string** that contains bolded text
this is *a string* that contains italicized text
> Dorothy followed her through many of the beautiful rooms in her castle.
> Dorothy followed her through many of the beautiful rooms in her castle.
>
>> The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.
> #### The quarterly results look great!
>
> - Revenue was off the chart.
> - Profits were higher than ever.
>
> *Everything* is going according to **plan**.
- First item
- Second item
- Third item
- Fourth item
`;
const { out: [html, update] } = createSource(createMarkdownParser(), createHtmlParser(), tempVal);
createEffect(() => {
console.log(html());
});
return <>
<div contentEditable innerHTML={html()} />
<Grid rows={rows()} locales={locales()} api={setApi}>{
key => { key => {
return <Context.Root commands={[copyKey.with(key)]}> return <Context.Root commands={[copyKey.with(key)]}>
<Context.Menu>{ <Context.Menu>{
@ -399,7 +434,8 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<Gr
<Context.Handle>{key.split('.').at(-1)!}</Context.Handle> <Context.Handle>{key.split('.').at(-1)!}</Context.Handle>
</Context.Root>; </Context.Root>;
} }
}</Grid>; }</Grid>
</>;
}; };
const Blank: Component<{ open: CommandType }> = (props) => { const Blank: Component<{ open: CommandType }> = (props) => {