From 57e7ff51a5aaa4f2c7b7bfff1712987374d34b6d Mon Sep 17 00:00:00 2001 From: Terah Date: Fri, 1 Mar 2024 21:58:15 +0100 Subject: [PATCH] Add sharing features --- package-lock.json | 65 +++++++++- package.json | 6 +- src/App.tsx | 2 +- src/logic/bytebuffer.ts | 203 ++++++++++++++++++++++++++++++++ src/{ => logic}/defaults.ts | 2 +- src/{ => logic}/parser.ts | 13 +- src/logic/sharing.ts | 116 ++++++++++++++++++ src/{ => logic}/wordgen.ts | 53 +-------- src/models/grammar.ts | 52 ++++++++ src/models/ui.ts | 28 +++++ src/pages/Home.tsx | 53 +++++++-- src/panels/Configuration.tsx | 5 +- src/panels/ExclusionSection.tsx | 29 ++--- src/panels/Generator.tsx | 48 +++++++- src/panels/RewriteSection.tsx | 30 ++--- src/panels/RuleInstance.tsx | 17 +-- src/panels/RuleSection.tsx | 38 +++--- 17 files changed, 610 insertions(+), 150 deletions(-) create mode 100644 src/logic/bytebuffer.ts rename src/{ => logic}/defaults.ts (99%) rename src/{ => logic}/parser.ts (97%) create mode 100644 src/logic/sharing.ts rename src/{ => logic}/wordgen.ts (79%) create mode 100644 src/models/grammar.ts create mode 100644 src/models/ui.ts diff --git a/package-lock.json b/package-lock.json index 3f90e48..648780b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,10 @@ "@headlessui/react": "^1.7.18", "@headlessui/tailwindcss": "^0.2.0", "@heroicons/react": "^2.1.1", + "@types/file-saver": "^2.0.7", + "base64-compressor": "^1.0.3", "clipboardy": "^4.0.0", + "file-saver": "^2.0.5", "find-cycle": "^1.1.0", "lodash.clonedeep": "^4.5.0", "react": "^18.2.0", @@ -24,7 +27,8 @@ "react-router-dom": "^6.22.1", "react-textarea-autosize": "^8.5.3", "react-toastify": "^10.0.4", - "uid": "^2.0.2" + "uid": "^2.0.2", + "use-file-picker": "^2.1.1" }, "devDependencies": { "@types/lodash.clonedeep": "^4.5.9", @@ -1414,6 +1418,11 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1435,6 +1444,17 @@ "@types/lodash": "*" } }, + "node_modules/@types/node": { + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", @@ -1824,6 +1844,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-compressor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/base64-compressor/-/base64-compressor-1.0.3.tgz", + "integrity": "sha512-xyLGTkukLML5YkWLX2GcwNEhBYVtnTlVhYe5eEY1q4bIjG0d1nrxl1RCedsLB2llBqjVzq6i4fA9vei1Ag55Jg==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2559,6 +2584,22 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, + "node_modules/file-selector": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz", + "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4348,6 +4389,14 @@ "node": ">=8" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -4395,6 +4444,20 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/use-file-picker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/use-file-picker/-/use-file-picker-2.1.1.tgz", + "integrity": "sha512-IhKUL7pVL5m0D/1zM5vgAyamkpiMfSQYg16+P7OpYEp1zTkhdX6jSsWr3kbbrfvPjSxkyaPy3lbCSyOslz9PNg==", + "dependencies": { + "file-selector": "0.2.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", diff --git a/package.json b/package.json index d5ea830..1c65966 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "@headlessui/react": "^1.7.18", "@headlessui/tailwindcss": "^0.2.0", "@heroicons/react": "^2.1.1", + "@types/file-saver": "^2.0.7", + "base64-compressor": "^1.0.3", "clipboardy": "^4.0.0", + "file-saver": "^2.0.5", "find-cycle": "^1.1.0", "lodash.clonedeep": "^4.5.0", "react": "^18.2.0", @@ -27,7 +30,8 @@ "react-router-dom": "^6.22.1", "react-textarea-autosize": "^8.5.3", "react-toastify": "^10.0.4", - "uid": "^2.0.2" + "uid": "^2.0.2", + "use-file-picker": "^2.1.1" }, "devDependencies": { "@types/lodash.clonedeep": "^4.5.9", diff --git a/src/App.tsx b/src/App.tsx index 792284f..e2d01ec 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ export const App = () => { }/> - }/> + }/> diff --git a/src/logic/bytebuffer.ts b/src/logic/bytebuffer.ts new file mode 100644 index 0000000..038c2d6 --- /dev/null +++ b/src/logic/bytebuffer.ts @@ -0,0 +1,203 @@ +// @ts-ignore +import {decodeBinary, encodeBinary} from 'base64-compressor' + +export class ByteBuffer { + private buffer: ArrayBuffer; + private view: DataView; + private wpos: number = 0; + private rpos: number = 0; + private endian: boolean; // true for LittleEndian, false for BigEndian + + constructor(data?: ArrayBuffer) { + this.buffer = data || new ArrayBuffer(0); + this.view = new DataView(this.buffer); + this.endian = true; // Default to LittleEndian + } + + public setEndian(littleEndian: boolean) { + this.endian = littleEndian; + } + + public writeBytes(bytes: Uint8Array) { + this.ensureCapacity(this.wpos + bytes.length); + + for (let i = 0; i < bytes.length; i++) { + this.view.setUint8(this.wpos + i, bytes[i]); + } + + this.wpos += bytes.length; + } + + public writeUint8(value: number) { + this.ensureCapacity(this.wpos + 1); + this.view.setUint8(this.wpos, value); + this.wpos += 1; + } + + public writeUint16(value: number) { + this.ensureCapacity(this.wpos + 2); + this.view.setUint16(this.wpos, value, this.endian); + this.wpos += 2; + } + + public writeUint32(value: number) { + this.ensureCapacity(this.wpos + 4); + this.view.setUint32(this.wpos, value, this.endian); + this.wpos += 4; + } + + public writeInt8(value: number) { + this.ensureCapacity(this.wpos + 1); + this.view.setInt8(this.wpos, value); + this.wpos += 1; + } + + public writeInt16(value: number) { + this.ensureCapacity(this.wpos + 2); + this.view.setInt16(this.wpos, value, this.endian); + this.wpos += 2; + } + + public writeInt32(value: number) { + this.ensureCapacity(this.wpos + 4); + this.view.setInt32(this.wpos, value, this.endian); + this.wpos += 4; + } + + public writeFloat32(value: number) { + this.ensureCapacity(this.wpos + 4); + this.view.setFloat32(this.wpos, value, this.endian); + this.wpos += 4; + } + + public writeFloat64(value: number) { + this.ensureCapacity(this.wpos + 8); + this.view.setFloat64(this.wpos, value, this.endian); + this.wpos += 8; + } + + public writeString(value: string) { + const encoder = new TextEncoder(); + const encodedString = encoder.encode(value); + this.writeUint8(encodedString.length); + this.writeBytes(encodedString); + } + + public writeText(value: string) { + const encoder = new TextEncoder(); + const encodedString = encoder.encode(value); + this.writeUint32(encodedString.length); + this.writeBytes(encodedString); + } + + public readUint8(): number { + const value = this.view.getUint8(this.rpos); + this.rpos += 1; + return value; + } + + public readUint16(): number { + const value = this.view.getUint16(this.rpos, this.endian); + this.rpos += 2; + return value; + } + + public readUint32(): number { + const value = this.view.getUint32(this.rpos, this.endian); + this.rpos += 4; + return value; + } + + public readInt8(): number { + const value = this.view.getInt8(this.rpos); + this.rpos += 1; + return value; + } + + public readInt16(): number { + const value = this.view.getInt16(this.rpos, this.endian); + this.rpos += 2; + return value; + } + + public readInt32(): number { + const value = this.view.getInt32(this.rpos, this.endian); + this.rpos += 4; + return value; + } + + public readFloat32(): number { + const value = this.view.getFloat32(this.rpos, this.endian); + this.rpos += 4; + return value; + } + + public readFloat64(): number { + const value = this.view.getFloat64(this.rpos, this.endian); + this.rpos += 8; + return value; + } + + public readString(): string { + const length = this.readUint8(); + const decoder = new TextDecoder(); + const value = decoder.decode(new Uint8Array(this.buffer, this.rpos, length)); + this.rpos += length; + return value; + } + + public readText(): string { + const length = this.readUint32(); + const decoder = new TextDecoder(); + const value = decoder.decode(new Uint8Array(this.buffer, this.rpos, length)); + this.rpos += length; + return value; + } + + // Helper method to ensure the buffer has enough capacity + private ensureCapacity(requiredCapacity: number) { + if (requiredCapacity <= this.buffer.byteLength) { + return; // No need to resize, there's enough space + } + + // Calculate new size: Either double the current size or make sure it's at least as large as requiredCapacity + let newSize = Math.max(this.buffer.byteLength * 2, requiredCapacity); + + // Allocate new buffer and copy existing data + let newBuffer = new ArrayBuffer(newSize); + new Uint8Array(newBuffer).set(new Uint8Array(this.buffer)); + + // Update internal references + this.buffer = newBuffer; + this.view = new DataView(this.buffer); + } + + public shrink() { + // Calculate the size needed to fit the data, which is the maximum of the read and write positions + const actualSize = Math.max(this.rpos, this.wpos); + + // If the actual size is less than the buffer's byte length, then we need to shrink the buffer + if (actualSize < this.buffer.byteLength) { + // Create a new ArrayBuffer that is just large enough to fit the data + let newBuffer = new ArrayBuffer(actualSize); + + // Copy the data from the old buffer to the new buffer + new Uint8Array(newBuffer).set(new Uint8Array(this.buffer, 0, actualSize)); + + // Update the internal buffer and DataView to point to the new, smaller ArrayBuffer + this.buffer = newBuffer; + this.view = new DataView(this.buffer); + } + } + + public async toBase64(): Promise { + this.shrink() + return await encodeBinary(this.buffer) + } + + public static async fromBase64(base64String: string): Promise { + const data = await decodeBinary(base64String) + return new ByteBuffer(data) + } + +} diff --git a/src/defaults.ts b/src/logic/defaults.ts similarity index 99% rename from src/defaults.ts rename to src/logic/defaults.ts index 90f6c33..f648cc4 100644 --- a/src/defaults.ts +++ b/src/logic/defaults.ts @@ -1,5 +1,5 @@ import {uid} from "uid"; -import {Rule} from "./panels/RuleInstance.tsx"; +import {Rule} from "../models/ui.ts"; export const tokiPonaRoot = "Word" diff --git a/src/parser.ts b/src/logic/parser.ts similarity index 97% rename from src/parser.ts rename to src/logic/parser.ts index 41b3709..5821302 100644 --- a/src/parser.ts +++ b/src/logic/parser.ts @@ -1,5 +1,7 @@ +import {Config} from "../pages/Home.tsx"; +import {Rule as ConfigRule} from "../models/ui.tsx"; import { - Expr, getRuleEdges, Grammar, + Expr, Grammar, makeAtom, makeGrammar, makeRange, makeRef, makeRule, @@ -7,10 +9,9 @@ import { makeWeighted, makeWeightedChoice, Rule, WeightedExpr -} from "./wordgen.ts"; -import {Config} from "./pages/Home.tsx"; -import {Rule as ConfigRule} from "./panels/RuleInstance.tsx"; -import {RulePattern} from "./panels/RuleSection.tsx"; +} from "../models/grammar.ts"; +import {getRuleEdges} from "./wordgen.ts"; +import {RulePattern} from "../models/ui.ts"; export class Parser { private currentPosition: number = 0; @@ -324,7 +325,7 @@ export const configToGrammar = (config: Config): Grammar | null => { for (const e of edges) { if (!rulesNames.includes(e)) { - throw new Error(`Uknown rule ${e}`) + throw new Error(`Unknown rule ${e}`) } } } diff --git a/src/logic/sharing.ts b/src/logic/sharing.ts new file mode 100644 index 0000000..1fb25c3 --- /dev/null +++ b/src/logic/sharing.ts @@ -0,0 +1,116 @@ +// @ts-ignore +import {Config} from '../pages/Home.tsx' +import {ExclusionPattern, RewritePattern, Rule, RulePattern} from "../models/ui.ts"; +import {uid} from "uid"; + +import {ByteBuffer} from "./bytebuffer.ts" + +export const encodeConfig = async (config: Config): Promise => { + + const buf = new ByteBuffer() + + buf.writeInt8(1) // Version + buf.writeString(config.root ?? "") + buf.writeInt8(config.enableWeights ? 1 : 0) + buf.writeInt8(config.enableSerif ? 1 : 0) + + // Rules + buf.writeInt8(config.rules.length) + for (const rule of config.rules) { + buf.writeString(rule.name) + buf.writeInt8(rule.terminalOnly ? 1 : 0) + buf.writeInt8(rule.showRewrites ? 1 : 0) + buf.writeInt8(rule.showExclusions ? 1 : 0) + + buf.writeInt8(rule.patterns.length) + for (const p of rule.patterns) { + buf.writeString(p.pattern) + buf.writeFloat32(p.weight) + } + + if (!rule.terminalOnly) { + buf.writeInt8(rule.rewrites.length) + for (const r of rule.rewrites) { + buf.writeString(r.match) + buf.writeString(r.replace) + } + + buf.writeInt8(rule.exclusions.length) + for (const e of rule.exclusions) { + buf.writeString(e.match) + } + } + } + + const data = await buf.toBase64() + + console.log(data) + return data +} + +export const decodeConfig = async (data: string): Promise => { + + + const buf = await ByteBuffer.fromBase64(data) + + // @ts-ignore + const _version = buf.readInt8() + const root = buf.readString() + const enableWeights = buf.readInt8() === 1 + const enableSerif = buf.readInt8() === 1 + + + const ruleCount = buf.readInt8() + let rules: Rule[] = [] + + for (let i = 0; i < ruleCount; i++) { + + const name = buf.readString() + const terminalOnly = buf.readInt8() === 1 + const showRewrites = buf.readInt8() === 1 + const showExclusions = buf.readInt8() === 1 + + const patternCount = buf.readInt8() + let patterns: RulePattern[] = [] + for (let n = 0; n < patternCount; n++) { + patterns.push({id: uid(), pattern: buf.readString(), weight: buf.readFloat32()}) + } + + let rewrites: RewritePattern[] = [] + let exclusions: ExclusionPattern[] = [] + + if (!terminalOnly) { + const rewriteCount = buf.readInt8() + for (let n = 0; n < rewriteCount; n++) { + rewrites.push({id: uid(), match: buf.readString(), replace: buf.readString()}) + } + + const exclusionCount = buf.readInt8() + for (let n = 0; n < exclusionCount; n++) { + exclusions.push({id: uid(), match: buf.readString()}) + } + } + + rules.push({ + id: uid(), + name: name, + terminalOnly: terminalOnly, + showRewrites: showRewrites, + showExclusions: showExclusions, + patterns: patterns, + exclusions: exclusions, + rewrites: rewrites, + }) + + + } + + const config = { + root: root, + rules: rules, + enableWeights: enableWeights, + enableSerif: enableSerif, + } + + return config +} \ No newline at end of file diff --git a/src/wordgen.ts b/src/logic/wordgen.ts similarity index 79% rename from src/wordgen.ts rename to src/logic/wordgen.ts index c485a6e..c7bce2b 100644 --- a/src/wordgen.ts +++ b/src/logic/wordgen.ts @@ -1,30 +1,5 @@ import findDirectedCycle from "find-cycle/directed" - -export type Expr = - | { tag: 'Atom', value: string } - | { tag: 'Ref', rule: string } - | { tag: 'Seq', items: Expr[] } - | { tag: 'Choice', items: WeightedExpr[] } - | { tag: 'Quantifier', expr: Expr, min: number, max: number } - - -export type WeightedExpr = { - expr: Expr - weight: number -} - -export interface Rule { - name: string; - expr: Expr; - exclusions: Expr[]; - rewrites: [Expr, Expr][]; -} - -export interface Grammar { - rules: Rule[]; - root: string; - useWeights: boolean -} +import {Expr, Grammar, Rule} from "../models/grammar.ts"; const matchSeq = (g: Grammar, items: Expr[], s: string): number | null => { if (items.length === 0) return 0; @@ -186,32 +161,6 @@ export const generate = (g: Grammar): string => { return generateRule(g, rootRule); }; -export const makeAtom = (atom: string): Expr => ({tag: "Atom", value: atom}); -export const makeRef = (rule: string): Expr => ({tag: "Ref", rule: rule}); -export const makeSeq = (items: Expr[]): Expr => ({tag: "Seq", items: items}); - -export const makeWeighted = (expr: Expr, weight: number | null): WeightedExpr => ({expr: expr, weight: weight ?? 1.0}) -export const makeChoice = (items: Expr[]): Expr => ({tag: "Choice", items: items.map(makeWeighted)}); -export const makeWeightedChoice = (items: WeightedExpr[]): Expr => ({tag: "Choice", items: items}); -export const makeRepeat = (expr: Expr, count: number): Expr => makeRange(expr, count, count) -export const makeRange = (expr: Expr, min: number, max: number): Expr => ({ - tag: "Quantifier", - expr: expr, - min: min, - max: max -}); -export const makeRule = (name: string, expr: Expr, exclusions: Expr[], rewrites: [Expr, Expr][]): Rule => ({ - name: name, - expr: expr, - exclusions: exclusions, - rewrites: rewrites -}) -export const makeGrammar = (root: string, rules: Rule[], useWeights: boolean = true): Grammar => ({ - rules: rules, - root: root, - useWeights: useWeights, -}) - export const countCombinations = (g: Grammar): number => { return countCombinationsRule(g, g.rules.find(r => r.name === g.root)!) } diff --git a/src/models/grammar.ts b/src/models/grammar.ts new file mode 100644 index 0000000..2648c00 --- /dev/null +++ b/src/models/grammar.ts @@ -0,0 +1,52 @@ +export type Grammar = { + rules: Rule[]; + root: string; + useWeights: boolean +} + +export type Rule = { + name: string; + expr: Expr; + exclusions: Expr[]; + rewrites: [Expr, Expr][]; +} + +export type Expr = + | { tag: 'Atom', value: string } + | { tag: 'Ref', rule: string } + | { tag: 'Seq', items: Expr[] } + | { tag: 'Choice', items: WeightedExpr[] } + | { tag: 'Quantifier', expr: Expr, min: number, max: number } + +export type WeightedExpr = { + expr: Expr + weight: number +} + +// Utility functions + +export const makeAtom = (atom: string): Expr => ({tag: "Atom", value: atom}); +export const makeRef = (rule: string): Expr => ({tag: "Ref", rule: rule}); +export const makeSeq = (items: Expr[]): Expr => ({tag: "Seq", items: items}); + +export const makeWeighted = (expr: Expr, weight: number | null): WeightedExpr => ({expr: expr, weight: weight ?? 1.0}) +export const makeChoice = (items: Expr[]): Expr => ({tag: "Choice", items: items.map(makeWeighted)}); +export const makeWeightedChoice = (items: WeightedExpr[]): Expr => ({tag: "Choice", items: items}); +export const makeRepeat = (expr: Expr, count: number): Expr => makeRange(expr, count, count) +export const makeRange = (expr: Expr, min: number, max: number): Expr => ({ + tag: "Quantifier", + expr: expr, + min: min, + max: max +}); +export const makeRule = (name: string, expr: Expr, exclusions: Expr[], rewrites: [Expr, Expr][]): Rule => ({ + name: name, + expr: expr, + exclusions: exclusions, + rewrites: rewrites +}) +export const makeGrammar = (root: string, rules: Rule[], useWeights: boolean = true): Grammar => ({ + rules: rules, + root: root, + useWeights: useWeights, +}) \ No newline at end of file diff --git a/src/models/ui.ts b/src/models/ui.ts new file mode 100644 index 0000000..1e4be33 --- /dev/null +++ b/src/models/ui.ts @@ -0,0 +1,28 @@ +export type Rule = { + id: string + name: string + patterns: RulePattern[] + rewrites: RewritePattern[] + exclusions: ExclusionPattern[] + terminalOnly: boolean + showRewrites: boolean + showExclusions: boolean +} + +export type RulePattern = { + id: string + pattern: string + weight: number +} + +export type RewritePattern = { + id: string + match: string, + replace: string +} + + +export type ExclusionPattern = { + id: string + match: string, +} \ No newline at end of file diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 31062af..ddfd950 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,16 +1,17 @@ -// import {useParams} from 'react-router-dom' import {Linear} from "../components/Linear.tsx"; import {Guide} from "../panels/Guide.tsx"; import {Configuration} from "../panels/Configuration.tsx"; import {Generator} from "../panels/Generator.tsx"; -import {useMemo, useState} from "react"; -import {Rule} from "../panels/RuleInstance.tsx"; +import {useEffect, useMemo, useState} from "react"; import {Bounce, ToastContainer} from "react-toastify"; import {Col} from "../components/Col.tsx"; import {Row} from "../components/Row.tsx"; -import {tokiPonaRoot, tokiPonaRules} from "../defaults.ts"; +import {tokiPonaRoot, tokiPonaRules} from "../logic/defaults.ts"; import {FaGithub} from "react-icons/fa"; -import {configToGrammar} from "../parser.ts"; +import {configToGrammar} from "../logic/parser.ts"; +import {Rule} from "../models/ui.ts"; +import {useParams} from "react-router-dom"; +import {encodeConfig, decodeConfig} from "../logic/sharing.ts" export type Config = { rules: Rule[], @@ -19,18 +20,49 @@ export type Config = { enableSerif: boolean, } - export const Home = () => { - // const params = useParams(); + const {urlConfig} = useParams(); const [config, setConfig] = useState({ - rules: tokiPonaRules, - root: tokiPonaRoot, + rules: [], + root: null, enableWeights: false, enableSerif: false, }) + useEffect(() => { + const updateUrl = async () => { + const data = await encodeConfig(config) + window.history.replaceState("", "", `/monke/${data}`); + } + + updateUrl().catch(console.error); + }, [config]) + + + useEffect(() => { + const updateConfigFromUrl = async () => { + console.log(urlConfig) + + if (urlConfig) { + const config = await decodeConfig(urlConfig) + console.log(config) + setConfig(config) + } else { + setConfig({ + rules: tokiPonaRules, + root: tokiPonaRoot, + enableWeights: false, + enableSerif: false, + }) + } + } + + updateConfigFromUrl().catch(console.error); + }, [urlConfig]) + + const setEnableWeights = (value: boolean) => { setConfig({...config, enableWeights: value}) } @@ -95,7 +127,8 @@ export const Home = () => { grammar={grammar} error={error} setEnableWeights={setEnableWeights} - setEnableSerif={setEnableSerif}/> + setEnableSerif={setEnableSerif} + setConfig={setConfig}/> diff --git a/src/panels/Configuration.tsx b/src/panels/Configuration.tsx index d04a620..1c31a93 100644 --- a/src/panels/Configuration.tsx +++ b/src/panels/Configuration.tsx @@ -1,5 +1,5 @@ import {Col} from "../components/Col.tsx"; -import {Rule, RuleInstance} from "./RuleInstance.tsx"; +import {RuleInstance} from "./RuleInstance.tsx"; import {uid} from "uid"; import cloneDeep from "lodash.clonedeep"; import {Row} from "../components/Row.tsx"; @@ -7,10 +7,11 @@ import {closestCenter, DndContext, DragEndEvent, useSensor, useSensors} from "@d import {arrayMove, SortableContext, verticalListSortingStrategy} from "@dnd-kit/sortable"; import {SmartPointerSensor} from "../components/SmartPointerSensor.ts"; import {Config} from "../pages/Home.tsx"; -import {tokiPonaRoot, tokiPonaRules, tokiPonaWeightedRules} from "../defaults.ts"; +import {tokiPonaRoot, tokiPonaRules, tokiPonaWeightedRules} from "../logic/defaults.ts"; import {Listbox, Transition} from "@headlessui/react"; import {Fragment} from "react"; import {CheckIcon, ChevronUpDownIcon} from "@heroicons/react/24/outline"; +import {Rule} from "../models/ui.ts"; type ConfigurationProps = { config: Config diff --git a/src/panels/ExclusionSection.tsx b/src/panels/ExclusionSection.tsx index d307691..3d1ef17 100644 --- a/src/panels/ExclusionSection.tsx +++ b/src/panels/ExclusionSection.tsx @@ -6,14 +6,9 @@ import {Row} from "../components/Row.tsx"; import {EllipsisVerticalIcon, PlusIcon, XMarkIcon} from "@heroicons/react/24/outline"; import AutowidthInput from "react-autowidth-input"; import {closestCenter, DndContext, DragEndEvent, PointerSensor, useSensor, useSensors} from "@dnd-kit/core"; -import {Rule} from "./RuleInstance.tsx"; import cloneDeep from "lodash.clonedeep"; import {uid} from "uid"; - -export type ExclusionPattern = { - id: string - match: string, -} +import {ExclusionPattern, Rule} from "../models/ui.ts"; export type ExclusionSectionProps = { @@ -75,10 +70,10 @@ export const ExclusionSection = ({rule, onRuleChange, enableSerif,}: GenericProp onDragEnd={onExclusionPatternDragEnd}> {rule.exclusions.map((e, index) => - + )} @@ -100,13 +95,13 @@ export type ExclusionPatternProps = { enableSerif?: boolean } -export const ExclusionPattern = ({ - className, - exclusion, - onChange, - onDelete, - enableSerif, - }: GenericProps) => { +export const ExclusionPatternItem = ({ + className, + exclusion, + onChange, + onDelete, + enableSerif, + }: GenericProps) => { const { listeners, diff --git a/src/panels/Generator.tsx b/src/panels/Generator.tsx index 82881e8..6da9fb1 100644 --- a/src/panels/Generator.tsx +++ b/src/panels/Generator.tsx @@ -3,11 +3,14 @@ import {SwitchLabel} from "../components/SwitchLabel.tsx"; import {Row} from "../components/Row.tsx"; import {ArrowDownTrayIcon, ArrowUpTrayIcon, ClipboardDocumentListIcon, LinkIcon} from "@heroicons/react/24/outline"; import {ChangeEvent, useMemo, useState} from "react"; -import {countCombinations, detectCycle, generate, Grammar} from "../wordgen.ts"; +import {countCombinations, detectCycle, generate} from "../logic/wordgen.ts"; import {Config} from "../pages/Home.tsx"; import clipboard from "clipboardy"; import {toast} from "react-toastify"; - +import {Grammar} from "../models/grammar.ts"; +import {saveAs} from 'file-saver'; +import {useFilePicker} from "use-file-picker"; +import {decodeConfig, encodeConfig} from "../logic/sharing.ts"; type GeneratorProps = { config: Config @@ -15,9 +18,10 @@ type GeneratorProps = { error: string | null setEnableWeights: (enable: boolean) => void setEnableSerif: (enable: boolean) => void + setConfig: (config: Config) => void } -export const Generator = ({config, grammar, error, setEnableWeights, setEnableSerif}: GeneratorProps) => { +export const Generator = ({config, grammar, error, setEnableWeights, setEnableSerif, setConfig}: GeneratorProps) => { const [generatedWords, setGeneratedWords] = useState([]) const [wordCount, setWordCount] = useState("50") @@ -25,6 +29,22 @@ export const Generator = ({config, grammar, error, setEnableWeights, setEnableSe const [filterDuplicates, setFilterDuplicates] = useState(false) const [generationError, setGenerationError] = useState(null) + + const {openFilePicker} = useFilePicker({ + accept: '.monke', + onFilesRejected: ({errors}) => { + console.log('Failed to import config', errors); + toast("Failed to import config") + }, + // @ts-ignore + onFilesSuccessfullySelected: ({filesContent}) => { + const data: string = filesContent[0].content + const newConfig = decodeConfig(data) + setConfig({...config, ...newConfig}) + toast("Config imported") + }, + }); + const filteredWords = useMemo( () => { if (filterDuplicates) { @@ -104,6 +124,22 @@ export const Generator = ({config, grammar, error, setEnableWeights, setEnableSe toast("Words copied to clipboard!") } + const onExport = () => { + const data = encodeConfig(config) + // @ts-ignore + const file = new File([data], "config.monke", {type: "text/plain;charset=utf-8"}); + saveAs(file); + } + + const onImport = () => { + openFilePicker() + } + + const onCopyLink = async () => { + await clipboard.write(window.location.href) + toast("Link copied to clipboard!") + } + return ( <> @@ -127,19 +163,19 @@ export const Generator = ({config, grammar, error, setEnableWeights, setEnableSe
- - -