got to a nice point, right now I can do bi-directional transformations, and also have my spelling and grammar error markers that are also cleaned up so they don't end up in the source text
This commit is contained in:
parent
8e0eee5847
commit
f4d59b30f5
20 changed files with 414 additions and 279 deletions
47
src/app.css
47
src/app.css
|
@ -2,19 +2,19 @@
|
|||
--hue: 182.77deg;
|
||||
--accent-ofset: 180;
|
||||
|
||||
--primary-100: oklch(from var(--primary-500) .95 c h);
|
||||
--primary-300: oklch(from var(--primary-500) .9 c h);
|
||||
--primary-500: light-dark(oklch(.7503 0.117 var(--hue)), oklch(.8549 0.1149 var(--hue)));
|
||||
--primary-600: oklch(from var(--primary-500) .7 c h);
|
||||
--primary-700: oklch(from var(--primary-500) .6 c h);
|
||||
--primary-900: oklch(from var(--primary-500) .35 calc(c + .15) h);
|
||||
--primary-100: oklch(from var(--primary-500) .35 c h);
|
||||
--primary-300: oklch(from var(--primary-500) .6 c h);
|
||||
--primary-500: light-dark(oklch(.7 0.117 var(--hue)), oklch(.7 0.1149 var(--hue)));
|
||||
--primary-600: oklch(from var(--primary-500) .85 c h);
|
||||
--primary-700: oklch(from var(--primary-500) .9 c h);
|
||||
--primary-900: oklch(from var(--primary-500) .95 calc(c + .15) h);
|
||||
|
||||
--secondary-100: oklch(from var(--primary-500) .95 c calc(h + var(--accent-ofset)));
|
||||
--secondary-300: oklch(from var(--primary-500) .9 c calc(h + var(--accent-ofset)));
|
||||
--secondary-500: oklch(from var(--primary-500) .85 c calc(h + var(--accent-ofset)));
|
||||
--secondary-600: oklch(from var(--primary-500) .7 c calc(h + var(--accent-ofset)));
|
||||
--secondary-700: oklch(from var(--primary-500) .6 c calc(h + var(--accent-ofset)));
|
||||
--secondary-900: oklch(from var(--primary-500) .35 calc(c + .15) calc(h + var(--accent-ofset)));
|
||||
--secondary-100: oklch(from var(--primary-500) .35 c calc(h + var(--accent-ofset)));
|
||||
--secondary-300: oklch(from var(--primary-500) .6 c calc(h + var(--accent-ofset)));
|
||||
--secondary-500: oklch(from var(--primary-500) .7 c calc(h + var(--accent-ofset)));
|
||||
--secondary-600: oklch(from var(--primary-500) .85 c calc(h + var(--accent-ofset)));
|
||||
--secondary-700: oklch(from var(--primary-500) .9 c calc(h + var(--accent-ofset)));
|
||||
--secondary-900: oklch(from var(--primary-500) .95 calc(c + .15) calc(h + var(--accent-ofset)));
|
||||
|
||||
--surface-300: light-dark(oklch(from var(--primary-500) .9 .02 h), oklch(from var(--primary-500) .2 .02 h));
|
||||
--surface-400: oklch(from var(--surface-300) calc(l + .025) c h);
|
||||
|
@ -162,6 +162,24 @@ del {
|
|||
color: oklch(from var(--fail) .1 .2 h);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
position: relative;
|
||||
padding: var(--padding-m);
|
||||
padding-inline-start: calc(.5em + var(--padding-m));
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
inset-block-start: 0;
|
||||
inline-size: .5em;
|
||||
block-size: 100%;
|
||||
background-color: var(--primary-600);
|
||||
}
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
background-color: var(--surface-600);
|
||||
|
@ -181,6 +199,11 @@ samp {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
[contenteditable][data-placeholder]:not(:focus):empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: oklch(from var(--text-2) l c h / .6)
|
||||
}
|
||||
|
||||
@supports ((scrollbar-width: auto) and (scrollbar-width: auto)) {
|
||||
:root {
|
||||
scrollbar-color: var(--surface-300) transparent;
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
const regex = /\w+\s+\w+/gi;
|
||||
export function defaultChecker(subject: string, lang: string): [number, number][] {
|
||||
return [];
|
||||
|
||||
return Array.from<RegExpExecArray>(subject.matchAll(regex)).filter(() => Math.random() >= .5).map(({ 0: match, index }) => {
|
||||
return [index, index + match.length - 1];
|
||||
});
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
const regex = /\w+/gi;
|
||||
export function defaultChecker(subject: string, lang: string): [number, number][] {
|
||||
return [];
|
||||
|
||||
return Array.from<RegExpExecArray>(subject.matchAll(regex)).filter(() => Math.random() >= .5).map(({ 0: match, index }) => {
|
||||
return [index, index + match.length - 1];
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.textarea {
|
||||
/* Make sure resizing works as intended */
|
||||
display: block;
|
||||
overflow: clip auto;
|
||||
resize: block;
|
||||
|
@ -9,16 +8,13 @@
|
|||
max-block-size: 50em;
|
||||
|
||||
unicode-bidi: plaintext;
|
||||
white-space-collapse: preserve;
|
||||
text-wrap-mode: wrap;
|
||||
overflow-wrap: break-word;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.spellingError {
|
||||
text-decoration-line: spelling-error;
|
||||
}
|
||||
& [data-marker="spelling"] {
|
||||
text-decoration-line: spelling-error;
|
||||
}
|
||||
|
||||
.grammarError {
|
||||
text-decoration-line: grammar-error;
|
||||
& [data-marker="grammar"] {
|
||||
text-decoration-line: grammar-error;
|
||||
}
|
||||
}
|
|
@ -1,109 +1,71 @@
|
|||
import { createEffect, createMemo, createSignal, untrack } from 'solid-js';
|
||||
import { decode } from '~/utilities';
|
||||
import { createEffect, createMemo, untrack } from 'solid-js';
|
||||
import { debounce } from '@solid-primitives/scheduled';
|
||||
import { createSelection } from '@solid-primitives/selection';
|
||||
import { defaultChecker as spellChecker } from './spellChecker';
|
||||
import { defaultChecker as grammarChecker } from './grammarChecker';
|
||||
import { createSource } from '~/features/source';
|
||||
import css from './textarea.module.css';
|
||||
|
||||
interface TextareaProps {
|
||||
class?: string;
|
||||
value: string;
|
||||
lang: string;
|
||||
oninput?: (event: InputEvent) => any;
|
||||
placeholder?: string;
|
||||
oninput?: (next: string) => any;
|
||||
spellChecker?: any;
|
||||
grammarChecker?: any;
|
||||
}
|
||||
|
||||
export function Textarea(props: TextareaProps) {
|
||||
const [selection, setSelection] = createSelection();
|
||||
const [value, setValue] = createSignal<string>(decode(props.value));
|
||||
const [element, setElement] = createSignal<HTMLTextAreaElement>();
|
||||
|
||||
const source = createSource(props.value);
|
||||
|
||||
createEffect(() => {
|
||||
setValue(decode(props.value));
|
||||
props.oninput?.(source.in);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
// For tracking
|
||||
value();
|
||||
source.in = props.value;
|
||||
});
|
||||
|
||||
const root = untrack(() => element());
|
||||
const onInput = debounce(() => {
|
||||
const [el, start, end] = untrack(() => selection());
|
||||
|
||||
if (el !== root) {
|
||||
return;
|
||||
if (el) {
|
||||
source.out = el.innerHTML;
|
||||
|
||||
el.style.height = `1px`;
|
||||
el.style.height = `${2 + el.scrollHeight}px`;
|
||||
|
||||
setSelection([el, start, end]);
|
||||
}
|
||||
|
||||
// TODO :: this needs to be calculated based on the modification done
|
||||
const offset = 1;
|
||||
|
||||
setSelection([el, start + offset, end + offset]);
|
||||
});
|
||||
|
||||
const resize = () => {
|
||||
const el = element();
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.height = `1px`;
|
||||
el.style.height = `${2 + element()!.scrollHeight}px`;
|
||||
};
|
||||
|
||||
const mutate = debounce(() => {
|
||||
props.oninput?.(new InputEvent('input', {
|
||||
data: value(),
|
||||
}))
|
||||
}, 300);
|
||||
|
||||
const onInput = (e: InputEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const spellingErrors = createMemo(() => spellChecker(source.out, props.lang));
|
||||
const grammarErrors = createMemo(() => grammarChecker(source.out, props.lang));
|
||||
|
||||
console.log(e);
|
||||
console.log(target.innerText, target.textContent, target.innerHTML);
|
||||
};
|
||||
// const html = createMemo(() => {
|
||||
// return source.out.split('').map((letter, index) => {
|
||||
// const spellingOpen = spellingErrors().some(([start]) => start === index) ? `<span class="${css.spellingError}">` : '';
|
||||
// const spellingClose = spellingErrors().some(([, end]) => end === index) ? `</span>` : '';
|
||||
|
||||
const onKeyUp = (e: KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
// const grammarOpen = grammarErrors().some(([start]) => start === index) ? `<span class="${css.grammarError}">` : '';
|
||||
// const grammarClose = grammarErrors().some(([, end]) => end === index) ? `</span>` : '';
|
||||
|
||||
setValue(element()!.textContent!);
|
||||
|
||||
resize();
|
||||
mutate();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// const spellingErrors = createMemo(() => spellChecker(value(), props.lang));
|
||||
// const grammarErrors = createMemo(() => grammarChecker(value(), props.lang));
|
||||
const spellingErrors = createMemo(() => []);
|
||||
const grammarErrors = createMemo(() => []);
|
||||
|
||||
const html = createMemo(() => {
|
||||
return value().split('').map((letter, index) => {
|
||||
const spellingOpen = spellingErrors().some(([start]) => start === index) ? `<span class="${css.spellingError}">` : '';
|
||||
const spellingClose = spellingErrors().some(([, end]) => end === index) ? `</span>` : '';
|
||||
|
||||
const grammarOpen = grammarErrors().some(([start]) => start === index) ? `<span class="${css.grammarError}">` : '';
|
||||
const grammarClose = grammarErrors().some(([, end]) => end === index) ? `</span>` : '';
|
||||
|
||||
return `${grammarOpen}${spellingOpen}${letter}${spellingClose}${grammarClose}`;
|
||||
}).join('');
|
||||
});
|
||||
// return `${grammarOpen}${spellingOpen}${letter}${spellingClose}${grammarClose}`;
|
||||
// }).join('');
|
||||
// });
|
||||
|
||||
return <div
|
||||
ref={setElement}
|
||||
class={`${css.textarea} ${props.class}`}
|
||||
lang={props.lang}
|
||||
contentEditable
|
||||
dir="auto"
|
||||
lang={props.lang}
|
||||
oninput={onInput}
|
||||
onkeyup={onKeyUp}
|
||||
innerHTML={source.out}
|
||||
data-placeholder={props.placeholder ?? ''}
|
||||
on:keydown={e => e.stopPropagation()}
|
||||
on:pointerdown={e => e.stopPropagation()}
|
||||
contentEditable
|
||||
innerHTML={html()}
|
||||
/>;
|
||||
}
|
|
@ -48,9 +48,16 @@ export function Grid(props: { class?: string, rows: Entry[], locales: string[],
|
|||
id: lang,
|
||||
label: lang,
|
||||
renderer: ({ row, column, value, mutate }) => {
|
||||
const entry = rows.value[row]!;
|
||||
const lang = String(column);
|
||||
const { key } = rows.value[row]!;
|
||||
|
||||
return <TextArea row={row} key={entry.key} lang={String(column)} value={value ?? ''} oninput={e => mutate(e.data ?? '')} />;
|
||||
return <Textarea
|
||||
class={css.textarea}
|
||||
value={value ?? ''}
|
||||
lang={lang}
|
||||
oninput={next => mutate(next)}
|
||||
placeholder={`${key} in ${lang}`}
|
||||
/>
|
||||
},
|
||||
}))
|
||||
]);
|
||||
|
@ -95,7 +102,7 @@ export function Grid(props: { class?: string, rows: Entry[], locales: string[],
|
|||
return <GridComp data={rows} columns={columns()} api={setApi} />;
|
||||
};
|
||||
|
||||
const TextArea: Component<{ row: number, key: string, lang: string, value: string, oninput?: (event: InputEvent) => any }> = (props) => {
|
||||
const TextArea: Component<{ row: number, key: string, lang: string, value: string, oninput?: (next: string) => any }> = (props) => {
|
||||
return <Textarea
|
||||
class={css.textarea}
|
||||
value={props.value}
|
||||
|
|
|
@ -7,7 +7,7 @@ interface Contents extends Map<string, Map<string, string>> { }
|
|||
|
||||
export const read = (file: File): Promise<Map<string, string> | undefined> => {
|
||||
switch (file.type) {
|
||||
case 'application/json': return json.load(file.stream())
|
||||
case 'application/json': return json.load(file.stream());
|
||||
|
||||
default: return Promise.resolve(undefined);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { decode } from "~/utilities";
|
||||
|
||||
export async function load(stream: ReadableStream<Uint8Array>): Promise<Map<string, string>> {
|
||||
return new Map(await Array.fromAsync(parse(stream), ({ key, value }) => [key, value]));
|
||||
|
@ -67,7 +68,7 @@ const states = {
|
|||
expect: 'key',
|
||||
});
|
||||
|
||||
next.entry = { key: path.join('.'), value: token.value };
|
||||
next.entry = { key: path.join('.'), value: decode(token.value) };
|
||||
|
||||
return next
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
|
||||
export type { Source } from './source';
|
||||
|
||||
export { createParser as createHtmlParser } from './parser/html';
|
||||
export { createParser as createMarkdownParser } from './parser/markdown';
|
||||
export { createSource } from './source';
|
|
@ -1,28 +0,0 @@
|
|||
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;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { Parser } from "../parser";
|
||||
|
||||
export function createParser(): Parser {
|
||||
return {
|
||||
parse(value) {
|
||||
return {};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
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++;
|
||||
}
|
||||
}
|
|
@ -1,31 +1,136 @@
|
|||
import { createEffect, createSignal, Signal } from "solid-js";
|
||||
import { Parser, RichTextAST } from "./parser";
|
||||
import { onMount } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { unified, Transformer } from 'unified'
|
||||
import { Node, Text, Element } from 'hast'
|
||||
import { visit } from "unist-util-visit";
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import remarkStringify from 'remark-stringify'
|
||||
import rehypeParse from 'rehype-dom-parse'
|
||||
import rehypeRemark from 'rehype-remark'
|
||||
import rehypeStringify from 'rehype-dom-stringify'
|
||||
|
||||
export interface Source<TIn extends Parser, TOut extends Parser> {
|
||||
readonly in: Signal<string>;
|
||||
readonly out: Signal<string>;
|
||||
export interface Source {
|
||||
in: string;
|
||||
out: 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>('');
|
||||
// TODO :: make this configurable, right now we can only do markdown <--> html.
|
||||
const inToOutProcessor = unified().use(remarkParse).use(remarkRehype).use(addErrors).use(rehypeStringify);
|
||||
const outToInProcessor = unified().use(rehypeParse).use(clearErrors).use(rehypeRemark).use(remarkStringify, { bullet: '-' });
|
||||
|
||||
const [ast, setAst] = createSignal<RichTextAST>();
|
||||
export function createSource(initalValue: string): Source {
|
||||
const [store, setStore] = createStore({ in: initalValue, out: '' });
|
||||
|
||||
createEffect(() => {
|
||||
setAst(inParser.parse(inValue()));
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
setAst(outParser.parse(outValue()));
|
||||
onMount(() => {
|
||||
setStore('out', String(inToOutProcessor.processSync(initalValue)));
|
||||
});
|
||||
|
||||
return {
|
||||
get in() {
|
||||
return [inValue, setIn] as Signal<string>;
|
||||
return store.in;
|
||||
},
|
||||
set in(next) {
|
||||
setStore({
|
||||
in: next,
|
||||
out: String(inToOutProcessor.processSync(next)),
|
||||
});
|
||||
},
|
||||
|
||||
get out() {
|
||||
return [outValue, setOut] as Signal<string>;
|
||||
return store.out;
|
||||
},
|
||||
set out(next) {
|
||||
setStore({
|
||||
in: String(outToInProcessor.processSync(next)).trim(),
|
||||
out: next,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function addErrors(): Transformer {
|
||||
const wrapInMarker = (text: Text, type: string): Element => ({
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {
|
||||
dataMarker: type,
|
||||
},
|
||||
children: [
|
||||
text
|
||||
]
|
||||
});
|
||||
|
||||
return function (tree) {
|
||||
visit(tree, n => n.type === 'text', (n, i, p: Element) => {
|
||||
if (typeof i !== 'number' || p === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errors = grammarChecker(n.value, 'en-GB');
|
||||
|
||||
if (errors.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
p.children.splice(i, 1, ...errors.map(([isHit, value]) => {
|
||||
const textNode: Text = { type: 'text', value };
|
||||
|
||||
return isHit ? wrapInMarker(textNode, 'grammar') : textNode;
|
||||
}))
|
||||
});
|
||||
|
||||
visit(tree, n => n.type === 'text', (n, i, p: Element) => {
|
||||
if (typeof i !== 'number' || p === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errors = spellChecker(n.value, 'en-GB');
|
||||
|
||||
if (errors.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
p.children.splice(i, 1, ...errors.map(([isHit, value]) => {
|
||||
const textNode: Text = { type: 'text', value };
|
||||
|
||||
return isHit ? wrapInMarker(textNode, 'spelling') : textNode;
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearErrors(): Transformer {
|
||||
const test = (n: Node) => n.type === 'element' && Object.hasOwn(n.properties, 'dataMarker');
|
||||
|
||||
return function (tree) {
|
||||
visit(tree, test, (n, i, p: Element) => {
|
||||
if (typeof i !== 'number' || p === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
p.children.splice(i, 1, ...n.children);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const spellChecker = checker(/\w+/gi);
|
||||
const grammarChecker = checker(/\w+\s+\w+/gi);
|
||||
|
||||
function checker(regex: RegExp) {
|
||||
return (subject: string, lang: string): (readonly [boolean, string])[] => {
|
||||
let lastIndex = 0;
|
||||
|
||||
return Array.from<RegExpExecArray>(subject.matchAll(regex)).filter(() => Math.random() >= .5).flatMap<readonly [boolean, string]>(({ 0: match, index }) => {
|
||||
const end = index + match.length;
|
||||
const result = [
|
||||
[false, subject.slice(lastIndex, index)],
|
||||
[true, subject.slice(index, end)],
|
||||
] as const;
|
||||
|
||||
lastIndex = end;
|
||||
|
||||
return result;
|
||||
}).concat([[false, subject.slice(lastIndex, subject.length)]]);
|
||||
}
|
||||
}
|
|
@ -390,52 +390,17 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<Gr
|
|||
|
||||
const copyKey = createCommand('page.edit.command.copyKey', (key: string) => writeClipboard(key));
|
||||
|
||||
const tempVal = `
|
||||
# Header
|
||||
return <Grid rows={rows()} locales={locales()} api={setApi}>{
|
||||
key => {
|
||||
return <Context.Root commands={[copyKey.with(key)]}>
|
||||
<Context.Menu>{
|
||||
command => <Command.Handle command={command} />
|
||||
}</Context.Menu>
|
||||
|
||||
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 => {
|
||||
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>
|
||||
</>;
|
||||
<Context.Handle>{key.split('.').at(-1)!}</Context.Handle>
|
||||
</Context.Root>;
|
||||
}
|
||||
}</Grid>;
|
||||
};
|
||||
|
||||
const Blank: Component<{ open: CommandType }> = (props) => {
|
||||
|
|
|
@ -16,6 +16,7 @@ export default function Experimental(props: ParentProps) {
|
|||
<Menu.Item command={goTo.withLabel('table').with('table')} />
|
||||
<Menu.Item command={goTo.withLabel('grid').with('grid')} />
|
||||
<Menu.Item command={goTo.withLabel('context-menu').with('context-menu')} />
|
||||
<Menu.Item command={goTo.withLabel('formatter').with('formatter')} />
|
||||
</Menu.Root>
|
||||
|
||||
{props.children}
|
||||
|
|
21
src/routes/(editor)/experimental/formatter.module.css
Normal file
21
src/routes/(editor)/experimental/formatter.module.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
.root {
|
||||
margin: 1em;
|
||||
padding: .5em;
|
||||
gap: 1em;
|
||||
display: grid;
|
||||
|
||||
grid: 100% / repeat(2, minmax(0, 1fr));
|
||||
|
||||
inline-size: calc(100% - 2em);
|
||||
block-size: calc(100% - 2em);
|
||||
|
||||
place-content: start;
|
||||
background-color: var(--surface-500);
|
||||
border-radius: var(--radii-xl);
|
||||
|
||||
& > * {
|
||||
overflow: auto;
|
||||
padding: .5em;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
43
src/routes/(editor)/experimental/formatter.tsx
Normal file
43
src/routes/(editor)/experimental/formatter.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { createSignal } from "solid-js";
|
||||
import { debounce } from "@solid-primitives/scheduled";
|
||||
import { Textarea } from "~/components/textarea";
|
||||
import css from './formatter.module.css';
|
||||
|
||||
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
|
||||
`;
|
||||
|
||||
export default function Formatter(props: {}) {
|
||||
const [value, setValue] = createSignal(tempVal);
|
||||
|
||||
const onInput = debounce((e: InputEvent) => {
|
||||
setValue((e.target! as HTMLTextAreaElement).value);
|
||||
}, 300);
|
||||
|
||||
return <div class={css.root}>
|
||||
<textarea oninput={onInput}>{value()}</textarea>
|
||||
<Textarea value={value()} oninput={setValue} lang="en-GB" />
|
||||
</div>;
|
||||
}
|
|
@ -12,7 +12,7 @@ export const splitAt = (subject: string, index: number): readonly [string, strin
|
|||
export function* gen__split_by_filter(subject: string, filter: string): Generator<readonly [boolean, string], void, unknown> {
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const { 0: match, index, ...rest } of subject.matchAll(new RegExp(filter, 'gmi'))) {
|
||||
for (const { 0: match, index } of subject.matchAll(new RegExp(filter, 'gmi'))) {
|
||||
const end = index + match.length;
|
||||
|
||||
yield [false, subject.slice(lastIndex, index)];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue