13 | We are using node , 16 | Chrome , 19 | and Electron . 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "app/main.js", 3 | "scripts": { 4 | "start": "electron app", 5 | "web": "vite", 6 | "build:web": "vite build", 7 | "build": "npm run build:web && electron-builder" 8 | }, 9 | "build": { 10 | "appId": "lu.taonan.plastic", 11 | "files": [ 12 | "**/*", 13 | "./dist/**/*" 14 | ] 15 | }, 16 | "dependencies": { 17 | "dayjs": "^1.10.4", 18 | "electron": "^11.3.0", 19 | "navigo": "^8.9.0", 20 | "svelte-routing": "^1.5.0", 21 | "textarea-caret": "^3.1.0" 22 | }, 23 | "devDependencies": { 24 | "@tsconfig/svelte": "^1.0.10", 25 | "autoprefixer": "^10.2.4", 26 | "electron-builder": "^22.9.1", 27 | "postcss": "^8.2.6", 28 | "rollup-plugin-svelte": "^7.1.0", 29 | "svelte": "^3.34.0", 30 | "svelte-preprocess": "^4.6.9", 31 | "tailwindcss": "^2.0.3", 32 | "ts-essentials": "^7.0.1", 33 | "typescript": "^4.2.2", 34 | "vite": "^2.0.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: { 4 | purge: ["web/**/*.svelte"], 5 | }, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "strcitNullCheck": true, 4 | "include": ["web/**/*", "web/node_modules"], 5 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"], 6 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import svelte from "rollup-plugin-svelte"; 2 | import autoPreprocess from "svelte-preprocess"; 3 | 4 | export default { 5 | root: "web", 6 | base: "./", 7 | plugins: [ 8 | svelte({ 9 | preprocess: autoPreprocess(), 10 | }), 11 | ], 12 | build: { 13 | outDir: "../app/dist", 14 | rollupOptions: {}, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /web/App.svelte: -------------------------------------------------------------------------------- 1 | 59 | 60 |{item.value}
11 |
--------------------------------------------------------------------------------
/web/editor/blocks/builtin/ExternalLink.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 | {title}
--------------------------------------------------------------------------------
/web/editor/blocks/builtin/Link.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 | [{item.value}]
11 |
--------------------------------------------------------------------------------
/web/editor/blocks/builtin/Todo.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
--------------------------------------------------------------------------------
/web/editor/directives.ts:
--------------------------------------------------------------------------------
1 | import getCaretCoordinates from 'textarea-caret'
2 |
3 | export function caret(el: HTMLTextAreaElement, cb) {
4 | const handler = function () {
5 | var caret = getCaretCoordinates(this, this.selectionEnd);
6 | cb(caret)
7 | };
8 |
9 | el.addEventListener('input', handler)
10 |
11 | return {
12 | destroy() {
13 | el.removeEventListener('input', handler)
14 | }
15 | }
16 | }
17 |
18 | export function clickOutside(el, cb) {
19 | function handler(e) {
20 | if (e.target !== el) {
21 | cb();
22 | }
23 | }
24 |
25 | document.addEventListener("click", handler);
26 |
27 | return {
28 | destroy() {
29 | document.removeEventListener("click", handler);
30 | },
31 | };
32 | }
33 |
34 | function resize({ target }) {
35 | target.style.height = "1px";
36 | target.style.height = +target.scrollHeight + "px";
37 | }
38 |
39 | export function autoResize(el) {
40 | resize({ target: el });
41 | el.style.overflow = "hidden";
42 | el.addEventListener("input", resize);
43 |
44 | return {
45 | destroy: () => el.removeEventListener("input", resize),
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/web/editor/parser/index.ts:
--------------------------------------------------------------------------------
1 | import Todo from "../blocks/builtin/Todo.svelte";
2 | import Code from "../blocks/builtin/Code.svelte";
3 | import Bold from "../blocks/builtin/Bold.svelte";
4 | import ExternalLink from "../blocks/builtin/ExternalLink.svelte";
5 |
6 | const builtinRules = [
7 | {
8 | match: /\`([^\`]*)\`/,
9 | processor(matched, position) {
10 | return {
11 | type: "CODE",
12 | meta: {
13 | component: Code,
14 | },
15 | position,
16 | value: matched[1],
17 | matched,
18 | };
19 | },
20 | },
21 | {
22 | match: /\*\*([^\*]*)\*\*/,
23 | processor(matched, position) {
24 | return {
25 | type: "BOLD",
26 | meta: {
27 | component: Bold,
28 | },
29 | value: matched[1],
30 | position,
31 | matched,
32 | };
33 | },
34 | },
35 | {
36 | match: /\{\{\{TODO\}\}\}/,
37 | processor(matched, position) {
38 | return {
39 | type: "TODO",
40 | meta: {
41 | component: Todo,
42 | props: {
43 | checked: false,
44 | },
45 | },
46 | position,
47 | matched,
48 | };
49 | },
50 | },
51 | {
52 | match: /\{\{\{DONE\}\}\}/,
53 | processor(matched, position) {
54 | return {
55 | type: "DONE",
56 | meta: {
57 | component: Todo,
58 | props: {
59 | checked: true,
60 | },
61 | },
62 | position,
63 | matched,
64 | };
65 | },
66 | },
67 | {
68 | match: /\[([^\]]+)\]\(([^\)]+)\)/,
69 | processor(matched, position) {
70 | return {
71 | type: "ExternalLink",
72 | meta: {
73 | component: ExternalLink,
74 | props: {
75 | title: matched[1],
76 | url: matched[2],
77 | },
78 | },
79 | position,
80 | matched,
81 | };
82 | }
83 | },
84 | ] as Rule[];
85 |
86 | export type Token = {
87 | type: string;
88 | position: number;
89 | matched: RegExpMatchArray;
90 | meta?: any;
91 | value: string
92 | };
93 |
94 | export type Rule = {
95 | match: RegExp;
96 | processor(matched: RegExpMatchArray, position: number): Token;
97 | };
98 |
99 |
100 | export function tokenizer(str: string, rules: Rule[]) {
101 | let matches = builtinRules.concat(rules);
102 |
103 | let position = 0;
104 | let toMatch = str;
105 | let tokens = [] as Token[];
106 |
107 | let text: string | null = null;
108 |
109 | whileLoop: while (toMatch.length > 0) {
110 | let matched: RegExpMatchArray;
111 | for (const rule of matches) {
112 | if ((matched = toMatch.match(new RegExp(`^${rule.match.source}`)))) {
113 | toMatch = toMatch.slice(matched[0].length);
114 | // clear plain
115 | if (text !== null) {
116 | // @ts-expect-error
117 | tokens.push({
118 | type: "TEXT",
119 | value: text,
120 | position,
121 | });
122 | position += text.length;
123 | text = null;
124 | }
125 |
126 | tokens.push(rule.processor(matched, position));
127 |
128 | position += matched[0].length;
129 |
130 | continue whileLoop;
131 | }
132 | }
133 |
134 | text !== null ? (text += toMatch[0]) : (text = toMatch[0]);
135 | toMatch = toMatch.slice(1);
136 | }
137 |
138 | if (text !== null) {
139 | // @ts-expect-error
140 | tokens.push({
141 | type: "TEXT",
142 | value: text,
143 | position,
144 | });
145 | position += text.length;
146 | }
147 |
148 | return tokens;
149 | }
150 |
--------------------------------------------------------------------------------
/web/editor/store.ts:
--------------------------------------------------------------------------------
1 | import { writable } from "svelte/store";
2 |
3 | export const editingBlockId = writable(null);
4 | export const anchorOffset = writable(null);
5 |
6 |
7 | export function onChangeBlock(block, root) {
8 | // NOTE: DON'T mutate the `block` and `root` unless you know what you're doing
9 | console.log(block, root)
10 | }
11 |
--------------------------------------------------------------------------------
/web/editor/utils.ts:
--------------------------------------------------------------------------------
1 | export function isInBracket(str: string, position: number) {
2 | const reg = /\[\[([^\]]+)\]\]/g;
3 | // +-2 is the offset of `[[` and `]]`
4 | // @ts-expect-error
5 | const ranges = Array.from(str.matchAll(reg)).map((_: any) => [
6 | _.index + 2,
7 | _.index + _[0].length - 2,
8 | ] as number[]);
9 | return ranges.find(([start, end]) => {
10 | return position >= start && position <= end;
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |