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
-