Feature/add bi directional parsing #35
8 changed files with 220 additions and 70 deletions
|
@ -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: {
|
||||||
|
|
|
@ -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()}
|
||||||
|
|
6
src/features/source/index.ts
Normal file
6
src/features/source/index.ts
Normal 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';
|
28
src/features/source/parser.ts
Normal file
28
src/features/source/parser.ts
Normal 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;
|
||||||
|
}
|
9
src/features/source/parser/html.ts
Normal file
9
src/features/source/parser/html.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { Parser } from "../parser";
|
||||||
|
|
||||||
|
export function createParser(): Parser {
|
||||||
|
return {
|
||||||
|
parse(value) {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
80
src/features/source/parser/markdown.ts
Normal file
80
src/features/source/parser/markdown.ts
Normal 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++;
|
||||||
|
}
|
||||||
|
}
|
31
src/features/source/source.ts
Normal file
31
src/features/source/source.ts
Normal 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>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,17 +390,52 @@ 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 = `
|
||||||
key => {
|
# Header
|
||||||
return <Context.Root commands={[copyKey.with(key)]}>
|
|
||||||
<Context.Menu>{
|
|
||||||
command => <Command.Handle command={command} />
|
|
||||||
}</Context.Menu>
|
|
||||||
|
|
||||||
<Context.Handle>{key.split('.').at(-1)!}</Context.Handle>
|
this is **a string** that contains bolded text
|
||||||
</Context.Root>;
|
|
||||||
}
|
this is *a string* that contains italicized text
|
||||||
}</Grid>;
|
|
||||||
|
> 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 => {
|
||||||
|
return <Context.Root commands={[copyKey.with(key)]}>
|
||||||
|
<Context.Menu>{
|
||||||
|
command => <Command.Handle command={command} />
|
||||||
|
}</Context.Menu>
|
||||||
|
|
||||||
|
<Context.Handle>{key.split('.').at(-1)!}</Context.Handle>
|
||||||
|
</Context.Root>;
|
||||||
|
}
|
||||||
|
}</Grid>
|
||||||
|
</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Blank: Component<{ open: CommandType }> = (props) => {
|
const Blank: Component<{ open: CommandType }> = (props) => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue