diff --git a/package-lock.json b/package-lock.json index 8993c48..0ae6dc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "blocknotes", "version": "1.0.0", + "hasInstallScript": true, "dependencies": { "@capacitor/app": "^5.0.3", "@capacitor/core": "^5.0.0", @@ -39,6 +40,7 @@ "@wordpress/prettier-config": "^4.0.0", "fsa-mock": "^1.0.0", "husky": "^8.0.3", + "patch-package": "^8.0.0", "prettier": "^3.3.1", "sharp-cli": "^4.2.0", "vite": "^5.2.12" @@ -6054,6 +6056,12 @@ "node": ">=10.0.0" } }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -6818,6 +6826,21 @@ "node": ">=10" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, "node_modules/clipboard": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", @@ -8626,6 +8649,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -9804,6 +9836,24 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/json-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz", + "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -9834,6 +9884,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -9858,6 +9917,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -10531,6 +10599,15 @@ "node": ">= 0.8.0" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10617,6 +10694,168 @@ "tslib": "^2.0.3" } }, + "node_modules/patch-package": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", + "dev": true, + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.0.33", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/patch-package/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/patch-package/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/patch-package/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/patch-package/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/path-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", @@ -12572,6 +12811,18 @@ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", diff --git a/package.json b/package.json index 86b9cef..92ef835 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "type": "module", "scripts": { + "postinstall": "patch-package", "start": "vite", "build": "vite build", "sync": "npm run build && cap sync", @@ -46,6 +47,7 @@ "@wordpress/prettier-config": "^4.0.0", "fsa-mock": "^1.0.0", "husky": "^8.0.3", + "patch-package": "^8.0.0", "prettier": "^3.3.1", "sharp-cli": "^4.2.0", "vite": "^5.2.12" diff --git a/patches/@wordpress+block-editor+13.0.0.patch b/patches/@wordpress+block-editor+13.0.0.patch new file mode 100644 index 0000000..61922aa --- /dev/null +++ b/patches/@wordpress+block-editor+13.0.0.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@wordpress/block-editor/build-module/components/rich-text/event-listeners/input-rules.js b/node_modules/@wordpress/block-editor/build-module/components/rich-text/event-listeners/input-rules.js +index 03436e6..225ec15 100644 +--- a/node_modules/@wordpress/block-editor/build-module/components/rich-text/event-listeners/input-rules.js ++++ b/node_modules/@wordpress/block-editor/build-module/components/rich-text/event-listeners/input-rules.js +@@ -112,7 +112,7 @@ export default (props => element => { + __unstableMarkLastChangeAsPersistent(); + onChange({ + ...transformed, +- activeFormats: value.activeFormats ++ // activeFormats: value.activeFormats + }); + __unstableMarkAutomaticChange(); + } diff --git a/src/content.css b/src/content.css index d76405b..90c863b 100644 --- a/src/content.css +++ b/src/content.css @@ -4,4 +4,4 @@ body { font-family: Hoefler Text; font-size: 20px; padding: 1px 1em; -} +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index ff60e9f..0938710 100644 --- a/src/index.js +++ b/src/index.js @@ -4,11 +4,15 @@ import { Preferences } from '@capacitor/preferences'; import { get } from 'idb-keyval'; import { createRoot } from 'react-dom/client'; import { registerCoreBlocks } from '@wordpress/block-library'; +import { registerFormatType } from '@wordpress/rich-text'; +import tagFormat from './tag-format'; import app from './app'; import '@wordpress/format-library'; +registerFormatType(tagFormat.name, tagFormat); + // It is needed for the appenders, this should be fixed in GB. import '@wordpress/block-editor/build-style/content.css'; diff --git a/src/read-write.js b/src/read-write.js index df608a0..9d499ff 100644 --- a/src/read-write.js +++ b/src/read-write.js @@ -61,23 +61,18 @@ function useDebouncedCallback(callback, delay) { return debouncedCallback; } -const tagRegex = /#[\p{L}\p{N}]+/gu; +const tagRegex = /(#[\p{L}\p{N}]+)<\/u>/gu; export function getTagsFromText(text) { - const textContent = text.replace(/<[^>]+>/g, ''); - const matches = textContent.match(tagRegex); + const matches = text.match(tagRegex); if (!matches) { return []; } - // Filter out numbers only. - return matches.filter((tag) => !/^#\d+$/.test(tag)); + return matches.map((match) => match.replace(/<\/?u>/g, '')); } export function stripTags(text) { - return text.replace(tagRegex, (match) => { - // Don't replace tags with just numbers. - return /^#\d+$/.test(match) ? match : ''; - }); + return text.replace(tagRegex, ''); } export function getTitleFromBlocks(blocks, second) { @@ -96,7 +91,8 @@ export function getTitleFromBlocks(blocks, second) { for (const block of blocks) { const html = getBlockContent(block); - const textContent = stripTags(html.replace(/<[^>]+>/g, '')) + const textContent = stripTags(html) + .replace(/<[^>]+>/g, '') .trim() .slice(0, 50); if (textContent) { diff --git a/src/sidebar.jsx b/src/sidebar.jsx index 8637be7..9b318b9 100644 --- a/src/sidebar.jsx +++ b/src/sidebar.jsx @@ -31,7 +31,7 @@ function getTitleFromText({ text, blocks, path }, second) { start = end + 1; // Strip HTML and trim the line - const strippedLine = stripTags(stripHTML(currentLine)).trim(); + const strippedLine = stripHTML(stripTags(currentLine)).trim(); // Check if the line has meaningful content if (strippedLine) { diff --git a/src/tag-format.js b/src/tag-format.js new file mode 100644 index 0000000..9488711 --- /dev/null +++ b/src/tag-format.js @@ -0,0 +1,65 @@ +import { applyFormat, removeFormat } from '@wordpress/rich-text'; + +const name = 'blocknote/tag'; + +function getTagRegex() { + return /#[\p{L}\p{N}]+/gu; +} + +export default { + name, + title: 'Tag', + tagName: 'u', + className: null, + __unstableInputRule(value) { + const { start, end, text, formats } = value; + + // Remove any stray formats. + let s = null; + formats.forEach((group, i) => { + const hasTag = group.some((format) => format.type === name); + if (hasTag) { + if (s === null) { + s = i; + } + } else if (s !== null) { + if (!getTagRegex().test(text.slice(s, i - 1))) { + value = removeFormat(value, name, s, i - 1); + } + s = null; + } + }); + + if (s !== null) { + if (!getTagRegex().test(text.slice(s, text.length))) { + value = removeFormat(value, name, s, text.length); + } + } + + if (start !== end) { + return value; + } + + const HASH = '#'; + if (text[start - 2] === HASH) { + if (getTagRegex().test(text.slice(start - 2, start))) { + return applyFormat(value, { type: name }, start - 2, start); + } + } + + const textBefore = text.slice(0, start); + const tagRegex = getTagRegex(); + + let match; + let lastMatch; + while ((match = tagRegex.exec(textBefore)) !== null) { + lastMatch = match; + } + + if (lastMatch && lastMatch.index + lastMatch[0].length === start - 1) { + return removeFormat(value, name, start - 1, start); + } + + return value; + }, +}; diff --git a/tests/mock.spec.js b/tests/mock.spec.js index 5890033..a1cb9d3 100644 --- a/tests/mock.spec.js +++ b/tests/mock.spec.js @@ -356,5 +356,61 @@ test.describe('Blocknotes', () => { expect(await getPaths(page)).toEqual(['folder']); }); + test.describe('tags', () => { + async function getInnerHTML(page) { + return await canvas(page) + .locator(':focus') + .evaluate((node) => node.innerHTML); + } + + test('can insert', async ({ page }) => { + await page.getByRole('button', { name: 'Pick Folder' }).click(); + + await page.keyboard.type('#'); + + expect(await getInnerHTML(page)).toBe('#'); + + await page.keyboard.type('ab'); + + expect(await getInnerHTML(page)).toBe( + '#ab' + ); + + await page.keyboard.type('.'); + + expect(await getInnerHTML(page)).toBe('#ab.'); + + await page.keyboard.press('Home'); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('ArrowRight'); + + await page.keyboard.type('.'); + + expect(await getInnerHTML(page)).toBe('#.ab.'); + }); + + test('can insert before text', async ({ page }) => { + await page.getByRole('button', { name: 'Pick Folder' }).click(); + + await page.keyboard.type('z'); + + await page.keyboard.press('ArrowLeft'); + + await page.keyboard.type('#'); + + expect(await getInnerHTML(page)).toBe('#z'); + + await page.keyboard.type('ab'); + + expect(await getInnerHTML(page)).toBe( + '#abz' + ); + + await page.keyboard.type('.'); + + expect(await getInnerHTML(page)).toBe('#ab.z'); + }); + }); + // Test if file saves after deleting the file. });