├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── img ├── noteworthy-comparison_16sept2020.png ├── noteworthy-tikzjax.gif ├── noteworthy_10aug2020.png ├── noteworthy_12july2021.png ├── noteworthy_16sept2020.png ├── noteworthy_17sept2020.png ├── prosemirror-math_display.gif └── prosemirror-math_inline.gif ├── justfile ├── noteworthy-electron ├── .editorconfig ├── .eslintignore ├── .npmrc ├── .prettierignore ├── README.md ├── build │ ├── entitlements.mac.plist │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── notarize.js ├── electron-builder.yml ├── electron.vite.config.ts ├── ignore.eslintrc.cjs ├── ignore.prettierrc.yaml ├── package.json ├── resources │ ├── icon.png │ ├── icon │ │ ├── noteworthy-icon-512.png │ │ └── nwt_large.png │ └── themes │ │ ├── theme-academic-light.css │ │ ├── theme-default-dark.css │ │ ├── theme-default-light.css │ │ └── theme-typewriter-light.css ├── src │ ├── citation-js.d.ts │ ├── common │ │ ├── commands │ │ │ └── commands.ts │ │ ├── dialog.ts │ │ ├── doctypes │ │ │ ├── doctypes.ts │ │ │ ├── markdown-doc.ts │ │ │ ├── parse-doc.ts │ │ │ └── seed-doc.ts │ │ ├── events.ts │ │ ├── extensions │ │ │ ├── editor-config.ts │ │ │ ├── extension-api.ts │ │ │ ├── extension.ts │ │ │ ├── mark-extensions.ts │ │ │ ├── node-extensions.ts │ │ │ └── noteworthy-extension.ts │ │ ├── files.ts │ │ ├── inputEvents.ts │ │ ├── ipc.ts │ │ ├── markdown │ │ │ ├── markdown-ast.ts │ │ │ ├── mdast-to-string.ts │ │ │ ├── mdast2prose.ts │ │ │ ├── prose2mdast.ts │ │ │ ├── remark-plugins │ │ │ │ ├── concrete │ │ │ │ │ ├── mdast-util-concrete.ts │ │ │ │ │ └── remark-concrete.ts │ │ │ │ ├── error │ │ │ │ │ └── remark-error.ts │ │ │ │ ├── unwrap-image │ │ │ │ │ └── remark-unwrap-image.ts │ │ │ │ └── wikilink │ │ │ │ │ ├── mdast-util-wikilink.ts │ │ │ │ │ └── remark-wikilink.ts │ │ │ └── unist-utils.ts │ │ ├── prompt │ │ │ └── prompt.ts │ │ ├── prosemirror │ │ │ ├── commands │ │ │ │ ├── demoteHeadingCmd.ts │ │ │ │ ├── insertTab.ts │ │ │ │ └── moveSelection.ts │ │ │ ├── inputRules.ts │ │ │ ├── steps.ts │ │ │ └── util │ │ │ │ └── mark-utils.ts │ │ ├── settings.ts │ │ ├── types.ts │ │ └── util │ │ │ ├── DefaultMap.ts │ │ │ ├── date.ts │ │ │ ├── equal.ts │ │ │ ├── hash.ts │ │ │ ├── ignore-dir.ts │ │ │ ├── ignore-file.ts │ │ │ ├── is-dir.ts │ │ │ ├── is-file.ts │ │ │ ├── non-void.ts │ │ │ ├── pathstrings.ts │ │ │ ├── pick.ts │ │ │ ├── random.ts │ │ │ └── to.ts │ ├── extensions │ │ ├── README.md │ │ ├── noteworthy-autocomplete │ │ │ ├── README.md │ │ │ ├── autocomplete-component.tsx │ │ │ ├── autocomplete-extension.tsx │ │ │ ├── autocomplete.css │ │ │ ├── img │ │ │ │ └── noteworthy-autocomplete.gif │ │ │ └── index.ts │ │ ├── noteworthy-codemirror-preview │ │ │ ├── codemirror-preview-extension.ts │ │ │ ├── codemirror-preview-nodeview.ts │ │ │ ├── codemirror-preview-plugin.ts │ │ │ ├── codemirror-preview-types.ts │ │ │ ├── codemirror-utils.ts │ │ │ └── index.ts │ │ ├── noteworthy-seed │ │ │ ├── index.ts │ │ │ └── seed-extension.ts │ │ └── noteworthy-tikzjax │ │ │ ├── index.ts │ │ │ ├── lib │ │ │ └── tikzjax │ │ │ │ ├── tikzjax.css │ │ │ │ └── tikzjax.js │ │ │ └── tikzjax-extension.ts │ ├── main │ │ ├── MainIPC.ts │ │ ├── app.ts │ │ ├── default-index.ts │ │ ├── fsal │ │ │ ├── fsal-system.ts │ │ │ ├── fsal-watcher.ts │ │ │ └── fsal.ts │ │ ├── global.d.ts │ │ ├── index.ts │ │ ├── ipc │ │ │ ├── citation.ts │ │ │ ├── dialog.ts │ │ │ ├── file.ts │ │ │ ├── lifecycle.ts │ │ │ ├── metadata.ts │ │ │ ├── navigation.ts │ │ │ ├── outline.ts │ │ │ ├── shell.ts │ │ │ ├── tag.ts │ │ │ ├── theme.ts │ │ │ └── workspace.ts │ │ ├── menus │ │ │ └── app-menu.ts │ │ ├── plugins │ │ │ ├── citation-plugin.ts │ │ │ ├── crossref-plugin.ts │ │ │ ├── metadata-plugin.ts │ │ │ ├── outline-plugin.ts │ │ │ ├── plugin-service.ts │ │ │ └── plugin.ts │ │ ├── theme │ │ │ └── theme-service.ts │ │ ├── windows │ │ │ └── MainWindow.ts │ │ └── workspace │ │ │ ├── workspace-service.ts │ │ │ └── workspace.ts │ ├── patch.d.ts │ ├── preload │ │ ├── preload.d.ts │ │ └── preload.ts │ └── renderer │ │ ├── index.html │ │ └── src │ │ ├── RendererIPC.ts │ │ ├── assets │ │ ├── editor.css │ │ ├── fonts │ │ │ ├── lora │ │ │ │ ├── Lora-Bold.woff2 │ │ │ │ ├── Lora-BoldItalic.woff2 │ │ │ │ ├── Lora-Italic.woff2 │ │ │ │ ├── Lora-Medium.woff2 │ │ │ │ ├── Lora-MediumItalic.woff2 │ │ │ │ ├── Lora-Regular.woff2 │ │ │ │ ├── Lora-SemiBold.woff2 │ │ │ │ ├── Lora-SemiBoldItalic.woff2 │ │ │ │ └── lora.css │ │ │ └── roboto │ │ │ │ ├── LICENSE.txt │ │ │ │ ├── Roboto-Black.ttf │ │ │ │ ├── Roboto-BlackItalic.ttf │ │ │ │ ├── Roboto-Bold.ttf │ │ │ │ ├── Roboto-BoldItalic.ttf │ │ │ │ ├── Roboto-Italic.ttf │ │ │ │ ├── Roboto-Light.ttf │ │ │ │ ├── Roboto-LightItalic.ttf │ │ │ │ ├── Roboto-Medium.ttf │ │ │ │ ├── Roboto-MediumItalic.ttf │ │ │ │ ├── Roboto-Regular.ttf │ │ │ │ ├── Roboto-Thin.ttf │ │ │ │ ├── Roboto-ThinItalic.ttf │ │ │ │ └── roboto.css │ │ ├── logo.svg │ │ └── main.css │ │ ├── codicon │ │ ├── codicon.css │ │ └── codicon.ttf │ │ ├── commandManager.ts │ │ ├── commands │ │ └── newFileCommand.tsx │ │ ├── editors │ │ ├── editor-markdown.tsx │ │ └── editor.ts │ │ ├── index.tsx │ │ ├── render.tsx │ │ ├── ui │ │ ├── Modal │ │ │ ├── modal.css │ │ │ └── modal.tsx │ │ ├── ModalNewFile │ │ │ ├── ModalNewFile.css │ │ │ └── ModalNewFile.tsx │ │ ├── bibliography.tsx │ │ ├── calendarTab.tsx │ │ ├── editorComponent.tsx │ │ ├── explorer.tsx │ │ ├── historyTab.tsx │ │ ├── loading.tsx │ │ ├── outlineTab.tsx │ │ ├── panelBacklinks.tsx │ │ ├── tag-search.tsx │ │ └── yamlEditor.tsx │ │ └── vite.d.ts ├── tsconfig.json ├── tsconfig.node.json └── tsconfig.web.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tests ├── markdown └── roundtrip.test.ts ├── mocks └── fsal-mock.ts ├── tsconfig.json └── workspace └── tags.test.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [benrbray] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | 5 | #patreon: # Replace with a single Patreon username 6 | #open_collective: # Replace with a single Open Collective username 7 | 8 | #ko_fi: # Replace with a single Ko-fi username 9 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 10 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 11 | #liberapay: # Replace with a single Liberapay username 12 | #issuehunt: # Replace with a single IssueHunt username 13 | #otechie: # Replace with a single Otechie username 14 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.tgz 4 | *.log 5 | .vscode 6 | tsc_out 7 | 8 | # Diagnostic reports (https://nodejs.org/api/report.html) 9 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | .DS_Store 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # node-waf configuration 29 | .lock-wscript 30 | 31 | # Compiled binary addons (https://nodejs.org/api/addons.html) 32 | build/Release 33 | 34 | # Dependency directories 35 | node_modules/ 36 | .pnpm-store/** 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # TypeScript cache 43 | *.tsbuildinfo 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | .env.test 63 | 64 | # parcel-bundler cache (https://parceljs.org/) 65 | .cache 66 | 67 | # next.js build output 68 | .next 69 | 70 | # nuxt.js build output 71 | .nuxt 72 | 73 | # vuepress build output 74 | .vuepress/dist 75 | 76 | # Serverless directories 77 | .serverless/ 78 | 79 | # FuseBox cache 80 | .fusebox/ 81 | 82 | # DynamoDB Local files 83 | .dynamodb/ 84 | 85 | # Webpack 86 | .webpack/ 87 | 88 | # Electron-Forge 89 | out/ -------------------------------------------------------------------------------- /img/noteworthy-comparison_16sept2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/img/noteworthy-comparison_16sept2020.png -------------------------------------------------------------------------------- /img/noteworthy-tikzjax.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/img/noteworthy-tikzjax.gif -------------------------------------------------------------------------------- /img/noteworthy_10aug2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/img/noteworthy_10aug2020.png -------------------------------------------------------------------------------- /img/noteworthy_12july2021.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/img/noteworthy_12july2021.png -------------------------------------------------------------------------------- /img/noteworthy_16sept2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/img/noteworthy_16sept2020.png -------------------------------------------------------------------------------- /img/noteworthy_17sept2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/img/noteworthy_17sept2020.png -------------------------------------------------------------------------------- /img/prosemirror-math_display.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/img/prosemirror-math_display.gif -------------------------------------------------------------------------------- /img/prosemirror-math_inline.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/img/prosemirror-math_inline.gif -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | clean: 2 | rm -rf node_modules 3 | rm -rf noteworthy-electron/node_modules 4 | rm -rf noteworthy-web/node_modules 5 | 6 | cd noteworthy-electron && pnpm run clean 7 | cd noteworthy-electron && pnpm run clean 8 | 9 | clean-install: clean 10 | pnpm install 11 | 12 | dev: 13 | cd noteworthy-electron && pnpm run dev -------------------------------------------------------------------------------- /noteworthy-electron/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /noteworthy-electron/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /noteworthy-electron/.npmrc: -------------------------------------------------------------------------------- 1 | # workaround for electron-builder's incompatability with pnpm 2 | # https://github.com/electron-userland/electron-builder/issues/6289#issuecomment-1042620422 3 | # https://github.com/pnpm/pnpm/issues/5724#issuecomment-1345780170 4 | node-linker=hoisted 5 | -------------------------------------------------------------------------------- /noteworthy-electron/.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /noteworthy-electron/README.md: -------------------------------------------------------------------------------- 1 | # noteworthy-vite 2 | 3 | An Electron application with Solid and TypeScript 4 | 5 | ## Recommended IDE Setup 6 | 7 | - [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 8 | 9 | ## Project Setup 10 | 11 | ### Install 12 | 13 | ```bash 14 | $ npm install 15 | ``` 16 | 17 | ### Development 18 | 19 | ```bash 20 | $ npm run dev 21 | ``` 22 | 23 | ### Build 24 | 25 | ```bash 26 | # For windows 27 | $ npm run build:win 28 | 29 | # For macOS 30 | $ npm run build:mac 31 | 32 | # For Linux 33 | $ npm run build:linux 34 | ``` 35 | -------------------------------------------------------------------------------- /noteworthy-electron/build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /noteworthy-electron/build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/build/icon.icns -------------------------------------------------------------------------------- /noteworthy-electron/build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/build/icon.ico -------------------------------------------------------------------------------- /noteworthy-electron/build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/build/icon.png -------------------------------------------------------------------------------- /noteworthy-electron/build/notarize.js: -------------------------------------------------------------------------------- 1 | const { notarize } = require('@electron/notarize') 2 | 3 | module.exports = async (context) => { 4 | if (process.platform !== 'darwin') return 5 | 6 | console.log('aftersign hook triggered, start to notarize app.') 7 | 8 | if (!process.env.CI) { 9 | console.log(`skipping notarizing, not in CI.`) 10 | return 11 | } 12 | 13 | if (!('APPLE_ID' in process.env && 'APPLE_ID_PASS' in process.env)) { 14 | console.warn('skipping notarizing, APPLE_ID and APPLE_ID_PASS env variables must be set.') 15 | return 16 | } 17 | 18 | const appId = 'com.electron.app' 19 | 20 | const { appOutDir } = context 21 | 22 | const appName = context.packager.appInfo.productFilename 23 | 24 | try { 25 | await notarize({ 26 | appBundleId: appId, 27 | appPath: `${appOutDir}/${appName}.app`, 28 | appleId: process.env.APPLE_ID, 29 | appleIdPassword: process.env.APPLEIDPASS 30 | }) 31 | } catch (error) { 32 | console.error(error) 33 | } 34 | 35 | console.log(`done notarizing ${appId}.`) 36 | } 37 | -------------------------------------------------------------------------------- /noteworthy-electron/electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.electron.app 2 | productName: noteworthy-vite 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' 12 | asarUnpack: 13 | - resources/** 14 | afterSign: build/notarize.js 15 | win: 16 | executableName: noteworthy-vite 17 | nsis: 18 | artifactName: ${name}-${version}-setup.${ext} 19 | shortcutName: ${productName} 20 | uninstallDisplayName: ${productName} 21 | createDesktopShortcut: always 22 | mac: 23 | entitlementsInherit: build/entitlements.mac.plist 24 | extendInfo: 25 | - NSCameraUsageDescription: Application requests access to the device's camera. 26 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 27 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 28 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 29 | dmg: 30 | artifactName: ${name}-${version}.${ext} 31 | linux: 32 | target: 33 | - AppImage 34 | - snap 35 | - deb 36 | maintainer: electronjs.org 37 | category: Utility 38 | appImage: 39 | artifactName: ${name}-${version}.${ext} 40 | npmRebuild: false 41 | publish: 42 | provider: generic 43 | url: https://example.com/auto-updates 44 | -------------------------------------------------------------------------------- /noteworthy-electron/electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite' 3 | import solid from 'vite-plugin-solid' 4 | 5 | // configuration for electron-vite 6 | // https://electron-vite.org/config/ 7 | export default defineConfig({ 8 | main: { 9 | plugins: [externalizeDepsPlugin()], 10 | build: { 11 | lib: { 12 | // compile main as an es module (*.mjs) 13 | entry: "src/main/index.ts", 14 | formats: ["es"] 15 | } 16 | }, 17 | resolve: { 18 | alias: { 19 | "@common" : resolve("src/common"), 20 | "@extensions" : resolve("src/extensions"), 21 | '@main' : resolve('src/main'), 22 | '@renderer' : resolve('src/renderer/src'), 23 | '@resources' : resolve('resources'), 24 | } 25 | } 26 | }, 27 | preload: { 28 | plugins: [externalizeDepsPlugin()], 29 | resolve: { 30 | alias: { 31 | "@common" : resolve("src/common") 32 | } 33 | } 34 | }, 35 | renderer: { 36 | resolve: { 37 | alias: { 38 | "@common" : resolve("src/common"), 39 | "@extensions" : resolve("src/extensions"), 40 | '@main' : resolve('src/main'), 41 | '@preload' : resolve('src/preload'), 42 | '@renderer' : resolve('src/renderer/src'), 43 | '@resources' : resolve('resources'), 44 | } 45 | }, 46 | plugins: [solid()] 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /noteworthy-electron/ignore.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | commonjs: true, 6 | es6: true, 7 | node: true 8 | }, 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true 13 | }, 14 | sourceType: 'module', 15 | ecmaVersion: 2021 16 | }, 17 | plugins: ['@typescript-eslint', 'solid'], 18 | extends: [ 19 | 'eslint:recommended', 20 | 'plugin:solid/typescript', 21 | 'plugin:@typescript-eslint/recommended', 22 | 'plugin:@typescript-eslint/eslint-recommended', 23 | 'plugin:prettier/recommended' 24 | ], 25 | rules: { 26 | '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }], 27 | '@typescript-eslint/explicit-function-return-type': 'error', 28 | '@typescript-eslint/explicit-module-boundary-types': 'off', 29 | '@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }], 30 | '@typescript-eslint/no-explicit-any': 'error', 31 | '@typescript-eslint/no-non-null-assertion': 'off', 32 | '@typescript-eslint/no-var-requires': 'off' 33 | }, 34 | overrides: [ 35 | { 36 | files: ['*.js'], 37 | rules: { 38 | '@typescript-eslint/explicit-function-return-type': 'off' 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /noteworthy-electron/ignore.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: true 3 | printWidth: 100 4 | trailingComma: none 5 | -------------------------------------------------------------------------------- /noteworthy-electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noteworthy", 3 | "version": "0.0.1", 4 | "description": "Markdown editor with excellent math support!", 5 | "repository": { 6 | "type": "git", 7 | "url": "github:benrbray/noteworthy" 8 | }, 9 | "keywords": [ 10 | "productivity", 11 | "note", 12 | "markdown", 13 | "zettelkasten" 14 | ], 15 | "author": { 16 | "name": "Benjamin R. Bray", 17 | "email": "benrbray@gmail.com" 18 | }, 19 | "license": "AGPL-3.0-or-later", 20 | "bugs": { 21 | "url": "https://github.com/benrbray/noteworthy/issues" 22 | }, 23 | "homepage": "https://github.com/benrbray/noteworthy#readme", 24 | "main": "./out/main/index.mjs", 25 | "scripts": { 26 | "clean": "rm -rf dist; rm -rf out", 27 | "format": "prettier --write .", 28 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 29 | "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", 30 | "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", 31 | "typecheck": "pnpm run typecheck:node && pnpm run typecheck:web", 32 | "start": "electron-vite preview", 33 | "dev": "electron-vite dev", 34 | "build": "pnpm run typecheck && electron-vite build", 35 | "preinstall": "npx only-allow pnpm", 36 | "ignore:postinstall": "electron-builder install-app-deps", 37 | "build:win": "pnpm run build && electron-builder --win --config", 38 | "build:mac": "pnpm run build && electron-builder --mac --config", 39 | "build:linux": "pnpm run build && electron-builder --linux --config" 40 | }, 41 | "dependencies": { 42 | "@benrbray/mdast-util-cite": "^1.1.0", 43 | "@benrbray/prosemirror-math": "^1.0.0", 44 | "@benrbray/remark-cite": "^1.1.0", 45 | "@citation-js/core": "^0.5.7", 46 | "@citation-js/date": "^0.5.1", 47 | "@citation-js/plugin-bibtex": "^0.5.7", 48 | "@citation-js/plugin-csl": "^0.5.7", 49 | "@codemirror/commands": "^6.3.3", 50 | "@codemirror/lang-cpp": "^6.0.2", 51 | "@codemirror/lang-java": "^6.0.1", 52 | "@codemirror/lang-javascript": "^6.2.2", 53 | "@codemirror/lang-json": "^6.0.1", 54 | "@codemirror/lang-python": "^6.1.5", 55 | "@codemirror/language": "^6.10.1", 56 | "@codemirror/legacy-modes": "^6.4.0", 57 | "@codemirror/state": "^6.4.1", 58 | "@codemirror/view": "^6.26.3", 59 | "@electron-toolkit/preload": "^1.0.3", 60 | "@electron-toolkit/utils": "^1.0.2", 61 | "buffer": "^6.0.3", 62 | "chokidar": "^3.6.0", 63 | "citation-js": "^0.5.7", 64 | "citeproc": "^2.4.63", 65 | "electron-store": "^7.0.3", 66 | "electron-util": "^0.14.2", 67 | "electron-window-state": "^5.0.3", 68 | "fuzzysort": "^2.0.4", 69 | "katex": "^0.16.10", 70 | "lodash": "^4.17.21", 71 | "mdast-util-from-markdown": "^0.8.5", 72 | "mdast-util-to-markdown": "^0.6.5", 73 | "micromark": "^2.11.4", 74 | "micromark-extension-wiki-link": "^0.0.4", 75 | "path-browserify": "^1.0.1", 76 | "process": "^0.11.10", 77 | "prosemirror-autocomplete": "https://gitpkg.now.sh/benrbray/curvenote-editor/packages/prosemirror-autocomplete?benrbray/cancelOnFirstSpace&scripts.postinstall=npm%20run%20build%3Acjs%20%26%26%20npm%20run%20build%3Aesm%20%26%26%20npm%20run%20declarations", 78 | "prosemirror-commands": "^1.5.2", 79 | "prosemirror-gapcursor": "^1.3.2", 80 | "prosemirror-history": "^1.4.0", 81 | "prosemirror-inputrules": "^1.4.0", 82 | "prosemirror-keymap": "^1.2.2", 83 | "prosemirror-model": "^1.20.0", 84 | "prosemirror-schema-basic": "^1.2.2", 85 | "prosemirror-schema-list": "^1.3.0", 86 | "prosemirror-state": "^1.4.3", 87 | "prosemirror-tables": "^1.3.7", 88 | "prosemirror-transform": "^1.8.0", 89 | "prosemirror-view": "^1.33.4", 90 | "remark": "^13.0.0", 91 | "remark-directive": "^1.0.1", 92 | "remark-footnotes": "^3.0.0", 93 | "remark-frontmatter": "^3.0.0", 94 | "remark-gfm": "^1.0.0", 95 | "remark-math": "^4.0.0", 96 | "remark-parse": "^9.0.0", 97 | "unified": "^9.2.2", 98 | "yaml": "^2.4.1" 99 | }, 100 | "devDependencies": { 101 | "@electron-toolkit/tsconfig": "^1.0.1", 102 | "@electron/asar": "^3.2.9", 103 | "@electron/notarize": "^1.2.4", 104 | "@types/electron-store": "^3.2.0", 105 | "@types/katex": "^0.16.7", 106 | "@types/lodash": "^4.17.0", 107 | "@types/mdast": "^3.0.15", 108 | "@types/mocha": "^10.0.6", 109 | "@types/node": "^18.19.31", 110 | "@types/path-browserify": "^1.0.2", 111 | "@types/unist": "^2.0.10", 112 | "@typescript-eslint/eslint-plugin": "^5.62.0", 113 | "@typescript-eslint/parser": "^5.62.0", 114 | "dedent-js": "^1.0.1", 115 | "del": "^7.1.0", 116 | "del-cli": "^5.1.0", 117 | "electron": "^29.3.0", 118 | "electron-builder": "^24.13.3", 119 | "electron-vite": "^2.1.0", 120 | "eslint": "^8.57.0", 121 | "eslint-config-prettier": "^8.10.0", 122 | "eslint-plugin-prettier": "^4.2.1", 123 | "eslint-plugin-solid": "^0.12.1", 124 | "mocha": "^10.4.0", 125 | "prettier": "^2.8.8", 126 | "remark-stringify": "^9.0.1", 127 | "solid-js": "^1.8.16", 128 | "typescript": "^5.4.5", 129 | "url-loader": "^4.1.1", 130 | "vite": "^4.5.3", 131 | "vite-plugin-solid": "^2.10.2" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /noteworthy-electron/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/resources/icon.png -------------------------------------------------------------------------------- /noteworthy-electron/resources/icon/noteworthy-icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/resources/icon/noteworthy-icon-512.png -------------------------------------------------------------------------------- /noteworthy-electron/resources/icon/nwt_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/resources/icon/nwt_large.png -------------------------------------------------------------------------------- /noteworthy-electron/resources/themes/theme-default-dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /** UI Colors **/ 3 | --color-bg: #1a2932; 4 | --color-bg-1: #0e1b22; /* e.g. sidebar */ 5 | --color-bg-2: #0e1b22; /* e.g. code_blocks */ 6 | --color-bg-3: #21303a; /* e.g. folder */ 7 | 8 | --color-bg-hover: #1a2932; 9 | --color-bg-selected: #616e79; 10 | --color-bg-highlight: #323b43; /* e.g. active file */ 11 | 12 | --color-text: #d0d5e0; 13 | --color-text-faint: #616e79; 14 | --color-text-fainter: #2f4a57; 15 | 16 | /** Search **/ 17 | --color-bg-textinput: #323b43; 18 | --color-textinput: var(--color-text); 19 | --color-text-fuzzy-match: #fac863; 20 | 21 | /** Semantic Colors **/ 22 | --color-good: #56a756; 23 | --color-bad: #e46e7c; 24 | --color-warning: #f0c05f; 25 | --color-neutral: steelblue; 26 | --color-question: #b47bac; 27 | --color-todo: #b47bac; 28 | 29 | /** Editor Colors **/ 30 | --color-link: steelblue; 31 | --color-wikilink: steelblue; 32 | --color-citation: seagreen; 33 | --color-definition: blue; 34 | --color-inline-delims: #616e79; 35 | --color-heading-level: #616e79; 36 | --color-math-src: #ad4fbd; 37 | } 38 | 39 | /* == Basic Element Styles ============================== */ 40 | 41 | /* headings */ 42 | #editor h1::before { content: "# "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 43 | #editor h2::before { content: "## "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 44 | #editor h3::before { content: "### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 45 | #editor h4::before { content: "#### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 46 | #editor h5::before { content: "##### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 47 | #editor h6::before { content: "###### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 48 | 49 | #editor p { 50 | text-align: justify; 51 | } 52 | 53 | /* -- wikilinks, tags, citations ------------------------ */ 54 | 55 | /* wikilinks */ 56 | .wikilink { font-family: var(--code-font); font-size: 0.95em; color: var(--color-wikilink); } 57 | .wikilink::before { content: "[["; color: var(--color-inline-delims); letter-spacing: -0.1em} 58 | .wikilink::after { content: "]]"; color: var(--color-inline-delims); letter-spacing: -0.1em} 59 | 60 | /* tags */ 61 | .tag { font-family: var(--font-editor); color: steelblue; } 62 | .tag::before { content: "#"; font-family: var(--code-font);} 63 | 64 | /* citation */ 65 | .citation { font-family: var(--code-font); font-size: 0.95em; color: seagreen; } 66 | .citation::before { content: "@["; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} 67 | .citation::after { content: "]"; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} 68 | 69 | /* citation: pandoc style */ 70 | .citation-pandoc::before { content: "["; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} 71 | .citation-pandoc::after { content: "]"; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} -------------------------------------------------------------------------------- /noteworthy-electron/resources/themes/theme-default-light.css: -------------------------------------------------------------------------------- 1 | /* == Basic Element Styles ============================== */ 2 | 3 | /* headings */ 4 | #editor h1::before { content: "# "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 5 | #editor h2::before { content: "## "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 6 | #editor h3::before { content: "### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 7 | #editor h4::before { content: "#### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 8 | #editor h5::before { content: "##### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 9 | #editor h6::before { content: "###### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 10 | 11 | #editor p { 12 | text-align: justify; 13 | } 14 | 15 | /* -- wikilinks, tags, citations ------------------------ */ 16 | 17 | /* wikilinks */ 18 | .wikilink { font-family: var(--code-font); font-size: 0.95em; color: var(--color-wikilink); } 19 | .wikilink::before { content: "[["; color: var(--color-inline-delims); letter-spacing: -0.1em} 20 | .wikilink::after { content: "]]"; color: var(--color-inline-delims); letter-spacing: -0.1em} 21 | 22 | /* tags */ 23 | .tag { font-family: var(--font-editor); color: steelblue; } 24 | .tag::before { content: "#"; font-family: var(--code-font);} 25 | 26 | /* citation */ 27 | .citation { font-family: var(--code-font); font-size: 0.95em; color: seagreen; } 28 | .citation::before { content: "@["; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} 29 | .citation::after { content: "]"; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} 30 | 31 | /* citation: pandoc style */ 32 | .citation-pandoc::before { content: "["; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} 33 | .citation-pandoc::after { content: "]"; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} -------------------------------------------------------------------------------- /noteworthy-electron/resources/themes/theme-typewriter-light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-editor: var(--code-font); 3 | } 4 | 5 | /* == Basic Element Styles ============================== */ 6 | 7 | /* headings */ 8 | #editor h1::before { content: "# "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 9 | #editor h2::before { content: "## "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 10 | #editor h3::before { content: "### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 11 | #editor h4::before { content: "#### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 12 | #editor h5::before { content: "##### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 13 | #editor h6::before { content: "###### "; color: var(--color-heading-level); font-family: var(--code-font); margin-left: 0.5rem; } 14 | 15 | /* -- wikilinks, tags, citations ------------------------ */ 16 | 17 | /* wikilinks */ 18 | .wikilink { font-family: var(--code-font); color: var(--color-wikilink); } 19 | .wikilink::before { content: "[["; color: var(--color-inline-delims); letter-spacing: -0.1em} 20 | .wikilink::after { content: "]]"; color: var(--color-inline-delims); letter-spacing: -0.1em} 21 | 22 | /* tags */ 23 | .tag { font-family: var(--font-editor); color: steelblue; } 24 | .tag::before { content: "#"; font-family: var(--code-font);} 25 | 26 | /* citation */ 27 | .citation { font-family: var(--code-font); color: seagreen; } 28 | .citation::before { content: "@["; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} 29 | .citation::after { content: "]"; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} 30 | 31 | /* citation: pandoc style */ 32 | .citation-pandoc::before { content: "["; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} 33 | .citation-pandoc::after { content: "]"; color: var(--color-inline-delims); letter-spacing: -0.1em; font-family: var(--code-font);} -------------------------------------------------------------------------------- /noteworthy-electron/src/common/commands/commands.ts: -------------------------------------------------------------------------------- 1 | import { CommunityExtensionCommands } from "@common/extensions/noteworthy-extension"; 2 | 3 | export type RegisteredCommands = InternalCommands & CommunityExtensionCommands; 4 | export type RegisteredCommandName = keyof RegisteredCommands; 5 | 6 | export type CommandSpec = { arg : A , result : R } 7 | 8 | export type CommandArg = 9 | RegisteredCommands[T] extends CommandSpec ? A : never; 10 | 11 | export type CommandResult = 12 | RegisteredCommands[T] extends CommandSpec ? R : never; 13 | 14 | export type CommandHandler = ( 15 | arg: CommandArg, 16 | resolveCommand: (result: CommandResult) => void, 17 | rejectCommand: () => void 18 | ) => Promise>; 19 | 20 | export interface InternalCommands { 21 | // intentionally empty, to be extended by module declarations 22 | } 23 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/dialog.ts: -------------------------------------------------------------------------------- 1 | // TypeScript >=3.4 supports "const contexts" to force narrow type inference, 2 | // used here in place of enums / reverse enums for maintainable dialog buttons. 3 | // (https://github.com/Microsoft/TypeScript/pull/29510) 4 | // (https://stackoverflow.com/questions/44497388/typescript-array-to-string-literal-type) 5 | 6 | export const DialogSaveDiscardOptions = ["Cancel", "Discard Changes", "Save As", "Save"] as const; -------------------------------------------------------------------------------- /noteworthy-electron/src/common/doctypes/doctypes.ts: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////// 2 | 3 | export interface IDoc { } 4 | 5 | // -- DocParser ----------------------------------------- // 6 | 7 | export interface IDocParser { 8 | parse(serialized:string):IDoc|null; 9 | } 10 | 11 | interface IDocClass { 12 | new (...args:any[]): T; 13 | parse(serialized:string): T|null; 14 | } 15 | 16 | export class DocParser implements IDocParser { 17 | 18 | constructor(private _classToCreate: IDocClass) { } 19 | 20 | parse(serialized:string): T|null { 21 | return this._classToCreate.parse(serialized); 22 | } 23 | } 24 | 25 | // -- AstParser ----------------------------------------- // 26 | 27 | export interface IAstParser { 28 | parseAST(serialized:string):IDoc|null; 29 | } 30 | 31 | interface IAstClass { 32 | new (...args:any[]): T; 33 | parseAST(serialized:string): T|null; 34 | } 35 | 36 | export class AstParser implements IDocParser { 37 | 38 | constructor(private _classToCreate: IAstClass) { } 39 | 40 | parse(serialized:string): T|null { 41 | return this._classToCreate.parseAST(serialized); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/doctypes/parse-doc.ts: -------------------------------------------------------------------------------- 1 | // import parsers 2 | import { MarkdownASTParser } from "./markdown-doc"; 3 | import { IDoc, IDocParser } from "./doctypes"; 4 | 5 | //////////////////////////////////////////////////////////////////////////////// 6 | 7 | /** TODO (2021-05-30) relocate parseAST elsewhere? only used by workspace */ 8 | export function parseAST(fileExt: string, fileContents: string):IDoc|null { 9 | // select parser 10 | let parser:IDocParser; 11 | switch(fileExt){ 12 | case ".md" : { parser = MarkdownASTParser; } break; 13 | case ".txt" : { parser = MarkdownASTParser; } break; 14 | default : { parser = MarkdownASTParser; } break; 15 | } 16 | 17 | // parse 18 | return parser.parse(fileContents) || null; 19 | } 20 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/doctypes/seed-doc.ts: -------------------------------------------------------------------------------- 1 | import { IDoc } from "./doctypes"; 2 | 3 | // TODO (Ben @ 2023/05/03) refactor so plugin types are available here 4 | // @ts-ignore 5 | import type { ICrossRefProvider } from "@main/plugins/crossref-plugin"; 6 | // @ts-ignore 7 | import type { IMetadataProvider, IMetadata } from "@main/plugins/metadata-plugin"; 8 | // @ts-ignore 9 | import type { Citation, ICitationProvider } from "@main/plugins/citation-plugin"; 10 | 11 | //////////////////////////////////////////////////////////////////////////////// 12 | 13 | export class SeedDoc implements IDoc, IMetadataProvider, ICitationProvider, ICrossRefProvider { 14 | 15 | private readonly _properties: { [key:string] : string }; 16 | 17 | constructor() { 18 | this._properties = {}; 19 | } 20 | 21 | /* ---- IMetadataProvider ---- */ 22 | public IS_METADATA_PROVIDER: true = true; 23 | 24 | getMetadata(): IMetadata { 25 | return this._properties; 26 | } 27 | 28 | /* ---- ICrossRefProvider ---- */ 29 | public IS_XREF_PROVIDER:true = true; 30 | 31 | getTagsDefined(): string[] { 32 | return []; 33 | } 34 | 35 | getTagsMentioned(): string[] { 36 | return []; 37 | } 38 | 39 | /* ---- ICitationProvider ---- */ 40 | public IS_CITATION_PROVIDER: true = true; 41 | 42 | getCitation(): Citation | null { 43 | return null; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/events.ts: -------------------------------------------------------------------------------- 1 | export enum FsalEvents { 2 | WORKSPACE_CHANGED = "fsal-workspace-changed", 3 | WORKSPACE_SYNC = "fsal-workspace-sync", 4 | FILETREE_CHANGED = "fsal-filetree-changed", 5 | STATE_CHANGED = "fsal-state-changed", 6 | CHOKIDAR_EVENT = "fsal-chokidar-event", 7 | GLOBAL_CHOKIDAR_EVENT = "fsal-global-chokidar-event", /* @todo (9/13/20) this is temporary, should be removed */ 8 | } 9 | 10 | export enum UserEvents { 11 | REQUEST_FILE_SAVE = "request-file-save", 12 | DIALOG_FILE_SAVEAS = "dialog-file-saveas", 13 | DIALOG_FILE_OPEN = "dialog-file-open", 14 | DIALOG_WORKSPACE_OPEN = "dialog-workspace-open", 15 | REQUEST_FILE_OPEN_HASH = "request-file-open-hash", 16 | REQUEST_FILE_OPEN_PATH = "request-file-open-path", 17 | REQUEST_TAG_OPEN = "request-tag-open", 18 | REQUEST_TAG_OPEN_OR_CREATE = "request-tag-open-or-create" 19 | } 20 | 21 | export enum EditorEvents { 22 | ASK_SAVE_DISCARD_CHANGES = "ask-save-discard-changes" 23 | } 24 | 25 | export enum MenuEvents { 26 | MENU_FILE_SAVE = "menu-file-save", 27 | MENU_FILE_SAVEAS = "menu-file-saveas" 28 | } 29 | 30 | export enum FileEvents { 31 | FILE_DID_SAVE = "file-did-save", 32 | FILE_DID_SAVEAS = "file-did-saveas", 33 | FILE_DID_OPEN = "file-did-open" 34 | } 35 | 36 | // event strings below defined by chokidar 37 | export enum ChokidarEvents { 38 | ALL = "all", 39 | ADD_FILE = "add", 40 | CHANGE_FILE = "change", 41 | UNLINK_FILE = "unlink", 42 | ADD_DIR = "addDir", 43 | UNLINK_DIR = "unlinkDir", 44 | ERROR = "error", 45 | READY = "ready" 46 | } 47 | 48 | export enum IpcEvents { 49 | NOTIFY = "notify", 50 | NOTIFY_ERROR = "notify-error", 51 | RENDERER_INVOKE = "renderer-invoke" 52 | } 53 | 54 | export enum AppEvents { 55 | APP_QUIT = "app-quit" 56 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/extensions/extension-api.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler, CommandArg, RegisteredCommandName, CommandResult } from "@common/commands/commands"; 2 | import { FileHash } from "@common/files"; 3 | 4 | export type TagSearchResult = { 5 | /** tag name **/ 6 | result: string, 7 | /** tag name, split into named chunks to reflect alignment with a query **/ 8 | resultEmphasized: { text: string, emph?: boolean }[]; 9 | } 10 | 11 | export interface NoteworthyExtensionApi { 12 | fuzzyTagSearch(query: string): Promise; 13 | 14 | /** 15 | * Opens the file creation modal, and returns once the modal has been submitted. 16 | * @returns File hash of the created file, if successful. 17 | */ 18 | createFileViaModal(): Promise; 19 | 20 | // use at initialization time only 21 | registerCommand( 22 | commandName: C, 23 | handler: CommandHandler 24 | ): void; 25 | 26 | executeCommand( 27 | commandName: C, 28 | arg: CommandArg 29 | ): Promise>; 30 | } 31 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/extensions/noteworthy-extension.ts: -------------------------------------------------------------------------------- 1 | import { NoteworthyExtensionApi } from "@common/extensions/extension-api"; 2 | import * as PS from "prosemirror-state"; 3 | 4 | //////////////////////////////////////////////////////////////////////////////// 5 | 6 | export interface NoteworthyExtensionSpec { 7 | name: Name; 8 | config?: { [D in Deps[number]] ?: CommunityExtensionConfig[D] }; 9 | } 10 | 11 | //////////////////////////////////////////////////////////////////////////////// 12 | 13 | /** https://stackoverflow.com/a/69756175/1444650 */ 14 | // type PickByType = { 15 | // [P in keyof T as T[P] extends Value | undefined ? P : never]: T[P] 16 | // } 17 | 18 | // interface HasName { name: string } 19 | 20 | export type RegisteredExtensions = CommunityExtensions; //PickByType 21 | export type RegisteredExtensionName = keyof RegisteredExtensions 22 | 23 | 24 | // mapping from Name -> Config 25 | type CommunityExtensionConfig = { 26 | [P in keyof RegisteredExtensions] ?: RegisteredExtensions[P] extends { config: unknown } ? RegisteredExtensions[P]["config"] : never 27 | } 28 | 29 | export interface CommunityExtensions { 30 | // intentionally empty, to be extended by module declarations 31 | } 32 | 33 | export interface CommunityExtensionCommands { 34 | // intentionally empty, to be extended by module declarations 35 | } 36 | 37 | //////////////////////////////////////////////////////////////////////////////// 38 | 39 | export abstract class NoteworthyExtension< 40 | Name extends RegisteredExtensionName, 41 | Deps extends RegisteredExtensionName[] = [], 42 | > { 43 | 44 | constructor() { } 45 | 46 | /** 47 | * Update the existing configuration with a new value. 48 | * It is up to the extension to decide whether to: 49 | * 1. completely replace the old value 50 | * 2. merge the old and new values 51 | */ 52 | abstract updateConfig(updated: NonNullable): void; 53 | 54 | /** 55 | * Return a set of [plugins](https://prosemirror.net/docs/ref/#state.Plugin_System) 56 | * to be attached to the editor's ProseMirror instance. 57 | * 58 | * Called *after* all extensions have been initialized and their configs updated. 59 | */ 60 | makeProseMirrorPlugins(): PS.Plugin[] { return []; } 61 | } 62 | 63 | //////////////////////////////////////////////////////////////////////////////// 64 | 65 | export interface NoteworthyExtensionInitializer< 66 | Name extends RegisteredExtensionName = RegisteredExtensionName, 67 | Deps extends RegisteredExtensionName[] = [] 68 | > { 69 | spec: NoteworthyExtensionSpec 70 | initialize: (state: { 71 | editorElt: HTMLElement, 72 | api: NoteworthyExtensionApi 73 | }) => NoteworthyExtension 74 | } 75 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/files.ts: -------------------------------------------------------------------------------- 1 | // -- Directory Entry ----------------------------------- // 2 | 3 | export type IDirEntry = IDirectory | IFileDesc; 4 | 5 | export type IDirEntryMeta = IDirectoryMeta | IFileMeta; 6 | 7 | // -- Directory ----------------------------------------- // 8 | 9 | export interface IDirectory { 10 | type: "directory"; 11 | // directory info 12 | children: IDirEntry[]; 13 | // common 14 | parent: IDirectory | null; 15 | path: string; 16 | name: string; 17 | hash: FileHash; 18 | modTime: number; 19 | } 20 | 21 | export interface IDirectoryMeta extends Omit { 22 | parentHash:string|null; 23 | childrenMeta:IDirEntryMeta[]; 24 | } 25 | 26 | export interface IWorkspaceDir extends IDirectory { 27 | workspaceDataPath:string; 28 | } 29 | 30 | export interface ISubDirectory extends IDirectory { 31 | parent: IDirectory; 32 | } 33 | 34 | // -- File ---------------------------------------------- //} 35 | 36 | export interface IFileDesc { 37 | type: "file"; 38 | // file info 39 | dirPath: string; 40 | ext: string; 41 | contents: string | null; 42 | creationTime: number; 43 | //lineFeed: "\n"|"\n\r"|"\r\n"; 44 | // common 45 | parent: IDirectory | null; 46 | path: string; 47 | name: string; 48 | hash: FileHash; 49 | modTime: number; 50 | } 51 | 52 | export interface IFileMeta extends Omit { 53 | parentHash: string|null; 54 | } 55 | 56 | export interface IFileWithContents extends IFileDesc { 57 | contents: string; 58 | } 59 | 60 | export class IUntitledFile implements Omit { 61 | type: "file" = "file"; 62 | name?: undefined; 63 | path?: undefined; 64 | dirPath?: undefined; 65 | parent?: undefined; 66 | modTime: -1; 67 | creationTime: -1; 68 | contents: string; 69 | 70 | constructor(contents?:string){ 71 | this.modTime = -1; 72 | this.creationTime = -1; 73 | this.contents = contents || ""; 74 | } 75 | } 76 | export type IPossiblyUntitledFile = IFileWithContents | IUntitledFile; 77 | 78 | export type FileHash = string; 79 | 80 | // -- Workspace ----------------------------------------- // 81 | 82 | export enum FileCmp { 83 | DELETED = -2, 84 | MODTIME_DECR = -1, 85 | MODTIME_EQUAL = 0, 86 | MODTIME_INCR = 1, 87 | ADDED = 2 88 | } 89 | 90 | //////////////////////////////////////////////////////////// 91 | 92 | /** 93 | * Re-interpret an IFileDesc object as an IFileMeta object. 94 | * TODO (2021-05-30) IFileDesc/IFileMeta should be composed together, rather than discriminated 95 | */ 96 | export function getFileMetadata(file: IFileDesc): IFileMeta { 97 | return { 98 | // TODO: (the below reasoning was copied from Notable -- is it applicable to Noteworthy?) 99 | // By only passing the hash, the object becomes 100 | // both lean AND it can be reconstructed into a 101 | // circular structure with NO overheads in the 102 | // renderer. 103 | 'parentHash': (file.parent) ? file.parent.hash : null, 104 | 'dirPath': file.dirPath, 105 | 'path': file.path, 106 | 'name': file.name, 107 | 'hash': file.hash, 108 | 'ext': file.ext, 109 | 'type': file.type, 110 | 'modTime': file.modTime, 111 | 'creationTime': file.creationTime, 112 | }; 113 | } 114 | 115 | /** 116 | * This function returns a sanitized, non-circular version of dirObject. 117 | * @param dir A directory descriptor 118 | */ 119 | export function getDirMetadata(dir:IDirectory):IDirectoryMeta { 120 | // Handle the children 121 | let children:IDirEntryMeta[] = dir.children.map((elem:IDirEntry) => { 122 | if (elem.type === 'directory') { 123 | return getDirMetadata(elem) 124 | } else { 125 | return getFileMetadata(elem) 126 | } 127 | }) 128 | 129 | return { 130 | // By only passing the hash, the object becomes 131 | // both lean AND it can be reconstructed into a 132 | // circular structure with NO overheads in the 133 | // renderer. 134 | "type": "directory", 135 | 'parentHash': (dir.parent) ? dir.parent.hash : null, 136 | 'path': dir.path, 137 | 'name': dir.name, 138 | 'hash': dir.hash, 139 | 'childrenMeta': children, 140 | 'modTime': dir.modTime 141 | } 142 | } 143 | 144 | /** 145 | * Returns a flattened list of all the files in the given directory object. 146 | */ 147 | export function getFlattenedFiles(dir:IDirectory): { [hash:string] : IFileMeta } { 148 | let queue:IDirectory[] = [dir]; 149 | let result: { [hash: string]: IFileMeta } = Object.create(null); 150 | 151 | while(queue.length > 0){ 152 | let dir = queue.pop(); 153 | if(!dir) break; 154 | 155 | for (let child of dir.children) { 156 | if (child.type == "directory") { 157 | queue.push(child); 158 | } else { 159 | result[child.hash] = getFileMetadata(child); 160 | } 161 | } 162 | } 163 | 164 | return result; 165 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/inputEvents.ts: -------------------------------------------------------------------------------- 1 | export enum MouseButton { 2 | LEFT = 0, 3 | RIGHT = 1, 4 | MIDDLE = 2, 5 | BACK = 3, 6 | FORWARD = 4 7 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/ipc.ts: -------------------------------------------------------------------------------- 1 | // Here is an attempt at type-safe ipc between render/main! 2 | // It's not bullet-proof, but it's better than shuffling strings around! 3 | // 4 | // related links: 5 | // 1. https://github.com/electron/electron/pull/18449 6 | // 2. https://github.com/electron/electron/pull/4522 7 | // 3. https://github.com/electron/electron/issues/3642 8 | // 4. https://stackoverflow.com/questions/47597982/send-sync-message-from-ipcmain-to-ipcrenderer-electron 9 | 10 | /* -- Types --------------------------------------------- */ 11 | 12 | export type FunctionPropertyNames = { 13 | [K in keyof T]: 14 | K extends string 15 | ? (T[K] extends Function ? K : never) 16 | : never 17 | }[keyof T]; 18 | 19 | /* -- Invoker ------------------------------------------- */ 20 | 21 | interface Invokable { 22 | invoke: (channel: string, ...args: any[]) => void; 23 | }; 24 | 25 | /** 26 | * Creates a Proxy which redirects any methods (of type T) 27 | * through the invoke() function of the given invokable 28 | * argument, allowing for type-checked ipc events! 29 | * 30 | * Set up to allow us to write type-safe event code like: 31 | * 32 | * interface H { addPerson(name:string, age:int) } 33 | * let proxy:H = invokerFor("CHANNEL", logPrefix, "EXTRA") 34 | * 35 | * proxy.addPerson("Andy", 51) 36 | * proxy.addPerson("Beth", 28) 37 | * 38 | * which will be the same as the following manual invocations: 39 | * 40 | * ipc.invoke("CHANNEL", "EXTRA", "addPerson", "Andy", 51) 41 | * ipc.invoke("CHANNEL", "EXTRA", "addPerson", "Beth", 28) 42 | * 43 | * @param ipc An invokeable object to be wrapped by the proxy. 44 | * @param channel The first argument to ipc.invoke(), representing an event channel. 45 | * @param logPrefix Prefix to use when logging invocations. 46 | * @param args The remaining arguments will be passed as arguments to invoke(), 47 | * and will appear before any arguments passed to a method called on the proxy. 48 | * This behavior is useful when we need to "route" messages through a hierarchy 49 | * on the receiving side. 50 | */ 51 | export function invokerFor(ipc: Invokable, channel:string="command", logPrefix:string, ...const_args:unknown[]): T { 52 | const proxy = new Proxy({ ipc }, { 53 | get(target, prop: FunctionPropertyNames) { 54 | return async (data: unknown) => { 55 | console.log(`[${logPrefix}] :: invoke event :: prop=${prop}, channel=${channel}, const_args=${const_args}`); 56 | let result = target.ipc.invoke(channel, ...const_args, prop, data); 57 | return result; 58 | } 59 | } 60 | }); 61 | 62 | // TODO (Ben @ 2023/04/30) any way to avoid cast here? 63 | // the cast *should* be sound, because of the way we build the proxy 64 | return proxy as T; 65 | } 66 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/markdown/mdast-to-string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted From: 3 | * https://github.com/syntax-tree/mdast-util-to-string 4 | */ 5 | 6 | import * as Mdast from "mdast"; 7 | import * as Md from "@common/markdown/markdown-ast"; 8 | import { unistIsParent, unistIsStringLiteral } from "@common/markdown/unist-utils"; 9 | 10 | //////////////////////////////////////////////////////////// 11 | 12 | /** 13 | * Get the text content of a node. 14 | * Prefer the node’s plain-text fields, otherwise serialize its children, 15 | * and if the given value is an array, serialize the nodes in it. 16 | */ 17 | export function mdastTextContent(node: Md.Node, options?: { includeImageAlt : boolean }): string { 18 | var {includeImageAlt = true} = options || {} 19 | return one(node, includeImageAlt) 20 | } 21 | 22 | function one(node: Md.Node, includeImageAlt: boolean): string { 23 | // handle degenerate nodes 24 | if(!node || typeof node !== "object") { return ""; } 25 | // look for a "value" 26 | if(unistIsStringLiteral(node)) { 27 | return node.value; 28 | } 29 | // look for an "alt" 30 | if(includeImageAlt && (node as Mdast.Image).alt !== undefined) { 31 | return (node as Mdast.Image).alt as string; 32 | } 33 | // otherwise, concatenate the node's children 34 | if(unistIsParent(node)) { 35 | let result = all(node.children, includeImageAlt); 36 | if(result) { return result }; 37 | } 38 | // the node itself might be an array 39 | if(Array.isArray(node)) { 40 | return all(node, includeImageAlt); 41 | } 42 | 43 | return ""; 44 | } 45 | 46 | function all(values: Md.Node[], includeImageAlt: boolean): string { 47 | var result: string[] = []; 48 | 49 | for(let idx = 0; idx < values.length; idx++ ) { 50 | result[idx] = one(values[idx], includeImageAlt) 51 | } 52 | 53 | return result.join('') 54 | } 55 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/markdown/remark-plugins/concrete/mdast-util-concrete.ts: -------------------------------------------------------------------------------- 1 | // unist / micromark / mdast 2 | import * as Uni from "unist"; 3 | import * as Md from "@common/markdown/markdown-ast"; 4 | import { Token } from "micromark/dist/shared-types"; 5 | import { Context } from "mdast-util-to-markdown"; 6 | 7 | //////////////////////////////////////////////////////////// 8 | 9 | export interface ThematicBreak extends Uni.Node { 10 | // from mdast 11 | type: 'thematicBreak'; 12 | // concrete syntax 13 | ruleContent?: string 14 | } 15 | 16 | export interface ListItem extends Md.Parent { 17 | // from mdast 18 | type: 'listItem'; 19 | checked?: boolean; 20 | spread?: boolean; 21 | children: Md.BlockContent[]; 22 | // concrete syntax 23 | marker?: string | undefined; 24 | } 25 | 26 | //////////////////////////////////////////////////////////// 27 | 28 | export function concreteFromMarkdown() { 29 | 30 | function top(stack: T[]) { 31 | return stack[stack.length - 1] 32 | } 33 | 34 | function point(d: Uni.Point) { 35 | return {line: d.line, column: d.column, offset: d.offset} 36 | } 37 | 38 | function opener(this: any, create: Function, and?: Function) { 39 | return open 40 | 41 | function open(this: any, token: Token) { 42 | enter.call(this, create(token), token) 43 | if (and) and.call(this, token) 44 | } 45 | } 46 | 47 | function enter(this: any, node: any, token: Token) { 48 | this.stack[this.stack.length - 1].children.push(node) 49 | this.stack.push(node) 50 | this.tokenStack.push(token) 51 | node.position = {start: point(token.start)} 52 | return node 53 | } 54 | 55 | // -- Thematic Break -------------------------------- // 56 | 57 | function enterThematicBreak(this: any, token: Token) { 58 | return { 59 | type: 'thematicBreak', 60 | ruleContent: undefined 61 | } 62 | } 63 | 64 | function exitThematicBreak(this: any, token: Token): void { 65 | let hruleNode: ThematicBreak = this.exit(token); 66 | hruleNode.ruleContent = this.sliceSerialize(token); 67 | } 68 | 69 | // -- List ------------------------------------------ // 70 | 71 | function exitListItemMarker(this: any, token: Token): void { 72 | // get the token used for the list marker 73 | let marker = this.sliceSerialize(token); 74 | // for unordered lists, save the marker in the ListItem node 75 | let listItem = top(this.stack) as Md.ListItem; 76 | let listNode = this.stack[this.stack.length - 2] as Md.List; 77 | 78 | if(!listNode.ordered) { 79 | listItem.marker = marker; 80 | } 81 | } 82 | 83 | // -------------------------------------------------- // 84 | 85 | return { 86 | enter: { 87 | thematicBreak: opener(enterThematicBreak) 88 | }, 89 | exit: { 90 | thematicBreak: exitThematicBreak, 91 | listItemMarker: exitListItemMarker 92 | } 93 | } 94 | } 95 | 96 | //////////////////////////////////////////////////////////// 97 | 98 | import checkRepeat from "mdast-util-to-markdown/lib/util/check-rule-repeat.js"; 99 | import checkRule from "mdast-util-to-markdown/lib/util/check-rule.js"; 100 | 101 | import checkBullet from "mdast-util-to-markdown/lib/util/check-bullet.js" 102 | import checkListItemIndent from "mdast-util-to-markdown/lib/util/check-list-item-indent.js" 103 | import flow from "mdast-util-to-markdown/lib/util/container-flow.js" 104 | import indentLines from "mdast-util-to-markdown/lib/util/indent-lines.js" 105 | 106 | const repeat = (s: string, n: number) => { 107 | return s.repeat(n); 108 | } 109 | 110 | export function concreteToMarkdown() { 111 | // -- Thematic Break -------------------------------- // 112 | 113 | function handleThematicBreak(node: ThematicBreak, parent: Uni.Node, context: Context): string { 114 | // determine hrule syntax 115 | let rule: string; 116 | if(node.ruleContent) { 117 | // preserve the original hrule syntax 118 | rule = node.ruleContent; 119 | } else { 120 | // default behavior from mdast 121 | rule = repeat( 122 | checkRule(context) + (context.options.ruleSpaces ? ' ' : ''), 123 | checkRepeat(context) 124 | ); 125 | rule = context.options.ruleSpaces ? rule.slice(0, -1) : rule; 126 | } 127 | 128 | return rule; 129 | } 130 | 131 | // -- List Item ------------------------------------- // 132 | 133 | /** 134 | * Same as the default ListItem serialization code, except 135 | * that we use the ListItem's `marker`, if present. 136 | * 137 | * https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/handle/list-item.js#L9 138 | */ 139 | function handleListItem(node: ListItem, parent: Md.List, context: Context) { 140 | // determine which bullet to use 141 | var bullet: string = node.marker || checkBullet(context) 142 | 143 | // handle ordered list numbering 144 | if (parent && parent.ordered) { 145 | bullet = 146 | ((parent.start !== undefined && parent.start > -1) ? parent.start : 1) + 147 | (context.options.incrementListMarker === false 148 | ? 0 149 | : parent.children.indexOf(node)) + 150 | '.' 151 | } 152 | 153 | // determine indentation 154 | var listItemIndent = checkListItemIndent(context) 155 | let size: number = bullet.length + 1 156 | 157 | if ( 158 | listItemIndent === 'tab' || 159 | (listItemIndent === 'mixed' && ((parent && parent.spread) || node.spread)) 160 | ) { 161 | size = Math.ceil(size / 4) * 4 162 | } 163 | 164 | let exit = context.enter('listItem') 165 | let value = indentLines(flow(node, context), map) 166 | exit() 167 | 168 | return value 169 | 170 | function map(line: string, index: number, blank: boolean): string { 171 | if (index) { 172 | return (blank ? '' : repeat(' ', size)) + line 173 | } 174 | 175 | return (blank ? bullet : bullet + repeat(' ', size - bullet.length)) + line 176 | } 177 | } 178 | 179 | // -------------------------------------------------- // 180 | 181 | return { 182 | handlers: { 183 | thematicBreak: handleThematicBreak, 184 | listItem: handleListItem 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/markdown/remark-plugins/concrete/remark-concrete.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Markdown syntax is redundant, allowing multiple ways to express 3 | * constructs like hrules, lists, and headings. The default MDAST 4 | * parser throws away all this syntactic information, preserving 5 | * only the meaning. 6 | * 7 | * For applications which load and save a user's personal Markdown 8 | * files, this behavior will erase the user's personal preferences 9 | * for syntax and document formatting, which is not ideal. 10 | * 11 | * Thankfully, the micromark tokenizers provide enough information 12 | * to reconstruct the original input, it's just that MDAST decides 13 | * to throw it away. This plugin preserves SOME, but not ALL, of 14 | * the concrete syntax information as part of the syntax tree. 15 | */ 16 | 17 | // unist / remark / mdast / micromark 18 | import * as Uni from "unist"; 19 | import * as Mdast from "mdast"; 20 | import { Context } from "mdast-util-to-markdown"; 21 | import { Handle, Options as ToMarkdownOptions } from 'mdast-util-to-markdown'; 22 | 23 | // project imports 24 | import { concreteFromMarkdown, concreteToMarkdown } from "./mdast-util-concrete"; 25 | 26 | //////////////////////////////////////////////////////////// 27 | 28 | export function remarkConcretePlugin (this:any, opts = {}): any { 29 | const data = this.data() 30 | 31 | function add (field:string, value:unknown) { 32 | if (data[field]) data[field].push(value) 33 | else data[field] = [value] 34 | } 35 | 36 | add('fromMarkdownExtensions', concreteFromMarkdown()) 37 | add('toMarkdownExtensions', concreteToMarkdown()) 38 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/markdown/remark-plugins/error/remark-error.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Remark plugin to serialize nodes that represent Markdown 3 | * parse errors or other unrecognized document fragments that 4 | * arise during the mdast -> prosemirror parsing process. 5 | * 6 | * During parsing, the raw Markdown source of these regions 7 | * is preserved as part of the document in a special error block. 8 | * When serializing, this plugin copies the contents of these 9 | * error blocks verbatim to the output. 10 | */ 11 | 12 | // unist / remark / mdast / micromark 13 | import * as Uni from "unist"; 14 | import { Context } from "mdast-util-to-markdown"; 15 | import { Handle, Options as ToMarkdownOptions } from 'mdast-util-to-markdown'; 16 | 17 | //////////////////////////////////////////////////////////// 18 | 19 | export function remarkErrorPlugin (this:any, opts = {}): any { 20 | const data = this.data() 21 | 22 | function add (field:string, value:unknown) { 23 | if (data[field]) data[field].push(value) 24 | else data[field] = [value] 25 | } 26 | 27 | add('toMarkdownExtensions', errorToMarkdown()) 28 | } 29 | 30 | //////////////////////////////////////////////////////////// 31 | 32 | /** 33 | * Represents a parse error or otherwise unrecognized content 34 | * whose string value should be reproduced verbatim in the output. 35 | * No escaping is performed when serializing this node. 36 | */ 37 | export interface MdError extends Uni.Node { 38 | type: "error", 39 | value: string 40 | } 41 | 42 | export function errorToMarkdown(): ToMarkdownOptions { 43 | 44 | function handler (node: MdError, _:unknown, context: Context) { 45 | // most remark extensions use this context enter/exit to 46 | // assist with escaping unsafe characters, but the goal 47 | // here is to reproduce the node's value verbatim. It's 48 | // not clear from the remark docs whether enter/exit has 49 | // some other purpose, so we still include it 50 | const exit = context.enter('error') 51 | exit() 52 | 53 | return node.value; 54 | } 55 | 56 | return { 57 | unsafe: [], 58 | handlers: { 59 | // as of (2021-05-07), the typings for Handle do not reflect 60 | // that the handler will be passed nodes of a specific type 61 | error: handler as unknown as Handle 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/markdown/remark-plugins/unwrap-image/remark-unwrap-image.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Remark plugin which transforms the AST to remove paragraph wrappers around 3 | * images, whenever the image is the only content in its paragraph. 4 | * 5 | * Useful for distinguishing between block and inline images. 6 | * 7 | * (Note: This behavior is non-standard. CommonMark has only inline images.) 8 | * 9 | * Adapted from remark-unwrap-image by wooorm: 10 | * https://github.com/remarkjs/remark-unwrap-images 11 | */ 12 | 13 | // unist / remark / mdast / micromark 14 | import * as Uni from "unist"; 15 | import unified from "unified"; 16 | 17 | // noteworthy 18 | import { visitTransformNodeType, whitespace, VisitorAction } from "@common/markdown/unist-utils"; 19 | import * as Md from "@common/markdown/markdown-ast"; 20 | 21 | //////////////////////////////////////////////////////////// 22 | 23 | export const remarkUnwrapImagePlugin: unified.Attacher = (proc) => { 24 | return (tree: Uni.Node) => { 25 | visitTransformNodeType(tree, 'paragraph', (node: Md.Paragraph, index: number, parent: Uni.Parent|null) => { 26 | if ( 27 | parent && 28 | typeof index === 'number' && 29 | containsImage(node) === CONTAINS_IMAGE 30 | ) { 31 | parent.children.splice(index, 1, ...node.children) 32 | return { action: VisitorAction.SKIP, continueFrom: index } 33 | } 34 | 35 | return { action: VisitorAction.CONTINUE } 36 | }) 37 | } 38 | } 39 | 40 | const UNKNOWN = 1; 41 | const CONTAINS_IMAGE = 2; 42 | const CONTAINS_OTHER = 3; 43 | 44 | /** 45 | * @param {Paragraph} node 46 | * @param {boolean} [inLink] 47 | * @returns {1|2|3} 48 | */ 49 | function containsImage(node: Md.Paragraph): 1|2|3 { 50 | let result: 1|2|3 = UNKNOWN; 51 | let index = -1 52 | 53 | while (++index < node.children.length) { 54 | const child = node.children[index] 55 | 56 | if (whitespace(child)) { 57 | // White space is fine. 58 | } else if (child.type === 'image' || child.type === 'imageReference') { 59 | result = CONTAINS_IMAGE; 60 | } else { 61 | return CONTAINS_OTHER; 62 | } 63 | } 64 | 65 | return result 66 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/markdown/remark-plugins/wikilink/remark-wikilink.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/landakram/remark-wiki-link 2 | 3 | import { syntax } from 'micromark-extension-wiki-link'; 4 | import { fromMarkdown, toMarkdown } from './mdast-util-wikilink'; 5 | 6 | let warningIssued: boolean = false; 7 | 8 | function wikiLinkPlugin (this:any, opts = {}): any { 9 | const data = this.data() 10 | 11 | function add (field:string, value:unknown) { 12 | if (data[field]) data[field].push(value) 13 | else data[field] = [value] 14 | } 15 | 16 | if (!warningIssued && 17 | ((this.Parser && 18 | this.Parser.prototype && 19 | this.Parser.prototype.blockTokenizers) || 20 | (this.Compiler && 21 | this.Compiler.prototype && 22 | this.Compiler.prototype.visitors))) { 23 | warningIssued = true 24 | console.warn( 25 | '[remark-wiki-link] Warning: please upgrade to remark 13 to use this plugin' 26 | ) 27 | } 28 | 29 | add('micromarkExtensions', syntax(opts)) 30 | add('fromMarkdownExtensions', fromMarkdown(opts)) 31 | add('toMarkdownExtensions', toMarkdown(opts)) 32 | } 33 | 34 | export { wikiLinkPlugin } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/prosemirror/commands/demoteHeadingCmd.ts: -------------------------------------------------------------------------------- 1 | import { Command as ProseCommand } from "prosemirror-state"; 2 | import { NodeType } from "prosemirror-model"; 3 | 4 | //////////////////////////////////////////////////////////// 5 | 6 | export function incrHeadingLevelCmd( 7 | incr:number, 8 | options:{ 9 | requireTextblockStart:boolean, 10 | requireEmptySelection:boolean, 11 | }, 12 | bottomType?:NodeType 13 | ):ProseCommand { 14 | return (state, dispatch, view) => { 15 | // only works for empty selection 16 | if(options.requireEmptySelection && !state.selection.empty){ return false; } 17 | 18 | // only works at start of heading block 19 | let { $anchor } = state.selection; 20 | if (options.requireTextblockStart) { 21 | if (view ? !view.endOfTextblock("backward", state) 22 | : $anchor.parentOffset > 0 ) 23 | { return false; } 24 | } 25 | 26 | // parent must be a heading 27 | let parent = $anchor.parent; 28 | /** @todo (9/27/20) revisit typings for node attributes */ 29 | //if(!isHeadingNode(parent)) { return false; } 30 | if(parent.type.name !== "heading"){ return false; } 31 | 32 | // get heading position 33 | let headingPos = $anchor.before($anchor.depth); 34 | let targetLevel = Math.min(6, Math.max(0, parent.attrs.level + incr)); 35 | 36 | // change heading to desired level, if positive 37 | if(targetLevel > 0){ 38 | if(dispatch){ 39 | dispatch(state.tr.setNodeMarkup( 40 | headingPos, undefined, 41 | { level: targetLevel } 42 | )); 43 | } 44 | 45 | return true; 46 | } 47 | 48 | // otherwise, demote heading to bottomType, when provided 49 | if(bottomType) { 50 | if(dispatch){ 51 | dispatch(state.tr.setNodeMarkup( 52 | headingPos, bottomType 53 | )); 54 | } 55 | return true; 56 | } 57 | 58 | return false; 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/prosemirror/commands/insertTab.ts: -------------------------------------------------------------------------------- 1 | import { Command as ProseCommand } from "prosemirror-state"; 2 | 3 | export const insertTab:ProseCommand = (state, dispatch, view) => { 4 | if(dispatch) dispatch(state.tr.deleteSelection().insertText("\t")); 5 | return true; 6 | }; -------------------------------------------------------------------------------- /noteworthy-electron/src/common/prosemirror/commands/moveSelection.ts: -------------------------------------------------------------------------------- 1 | import { Command as ProseCommand } from "prosemirror-state"; 2 | 3 | /** @todo (10/4/20) code review wanted for this command */ 4 | export const moveSelection = (dir:(1|-1)): ProseCommand => { 5 | return (state, dispatch, view) => { 6 | // get selection 7 | let { $from, $to } = state.selection; 8 | let fromIndex = $from.index(0); 9 | let toIndex = $to.index(0); 10 | 11 | // if dir > 0, 12 | // find the top-level block AFTER the selection 13 | // and move it to BEFORE the selection 14 | // if dir < 0, 15 | // find the top-level block BEFORE the selection 16 | // and move it to AFTER the selection 17 | 18 | // compute index of 19 | let MOVING_DOWN = (dir > 0); 20 | let moveNodeIndex = (MOVING_DOWN ? toIndex + 1 : fromIndex - 1); 21 | let insertIndex = (MOVING_DOWN ? fromIndex : toIndex + 1); 22 | 23 | // ensure there is a node at the index 24 | let maxIndex = state.doc.content.content.length; 25 | if(moveNodeIndex < 0 || moveNodeIndex >= maxIndex){ return true; } 26 | 27 | // compute move / insert positions 28 | /** @note posAtIndex does not check whether the input index is valid, 29 | * so it is crucial that we perform this validation ourselves, as above 30 | */ 31 | let moveNodePos = (MOVING_DOWN ? $to : $from).posAtIndex(moveNodeIndex, 0); 32 | let insertPos = (MOVING_DOWN ? $from : $to).posAtIndex(insertIndex, 0); 33 | 34 | let moveNode = state.doc.nodeAt(moveNodePos); 35 | if(!moveNode) { return false; } 36 | 37 | if(dispatch){ 38 | /** @todo (10/4/20) does this change guarantee a valid document wrt the schema? */ 39 | let tr = state.tr.insert(insertPos, moveNode); 40 | tr = tr.deleteRange( 41 | tr.mapping.map(moveNodePos), 42 | tr.mapping.map(moveNodePos+moveNode.nodeSize) 43 | ); 44 | dispatch(tr); 45 | } 46 | 47 | return true; 48 | }; 49 | } 50 | 51 | export const moveSelectionUp = () => moveSelection(-1); 52 | export const moveSelectionDown = () => moveSelection(+1); -------------------------------------------------------------------------------- /noteworthy-electron/src/common/prosemirror/inputRules.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from `prosemirror-inputrules` package. 3 | * https://github.com/ProseMirror/prosemirror-inputrules/blob/master/src/inputrules.ts 4 | */ 5 | 6 | import * as PI from "prosemirror-inputrules"; 7 | import * as PV from "prosemirror-view"; 8 | import * as PS from "prosemirror-state"; 9 | 10 | //////////////////////////////////////////////////////////// 11 | 12 | export type InputRulePluginState = { 13 | transform: PS.Transaction, 14 | from: number, 15 | to: number, 16 | text: string 17 | } | null; 18 | 19 | /** Create an input rules plugin. When enabled, it will cause text 20 | * input that matches any of the given rules to trigger the rule's 21 | * action. 22 | * 23 | * (unchanged from prosemirror-inputrules except for handleKeyDown) 24 | */ 25 | export function makeInputRulePlugin({rules}: {rules: readonly PI.InputRule[]}) { 26 | let plugin: PS.Plugin = new PS.Plugin({ 27 | state: { 28 | init() { return null }, 29 | apply(this: typeof plugin, tr, prev) { 30 | let stored = tr.getMeta(this) 31 | if (stored) return stored 32 | return tr.selectionSet || tr.docChanged ? null : prev 33 | } 34 | }, 35 | 36 | props: { 37 | handleTextInput(view, from, to, text) { 38 | return run(view, from, to, text, rules, plugin) 39 | }, 40 | handleDOMEvents: { 41 | compositionend: (view) => { 42 | setTimeout(() => { 43 | let {$cursor} = view.state.selection as PS.TextSelection 44 | if ($cursor) run(view, $cursor.pos, $cursor.pos, "", rules, plugin) 45 | }) 46 | 47 | // resolve type error 48 | // https://discuss.prosemirror.net/t/settimeout-in-inputrule-compositionend/3238 49 | return false; 50 | } 51 | }, 52 | // extend input rule regexes with the ability to handle newlines 53 | // https://discuss.prosemirror.net/t/trigger-inputrule-on-enter/1118/5 54 | handleKeyDown(view, event) { 55 | if (event.key !== "Enter") return false; 56 | let {$cursor} = view.state.selection as PS.TextSelection 57 | if ($cursor) return run(view, $cursor.pos, $cursor.pos, "\n", rules, plugin) 58 | return false; 59 | } 60 | }, 61 | 62 | // TODO (Ben @ 2023/04/04) revisit this ignore once https://github.com/benrbray/noteworthy/issues/31 63 | // @ts-ignore ts(2345) can only assign known properties 64 | isInputRules: true 65 | }) 66 | 67 | plugin.props 68 | 69 | return plugin 70 | } 71 | 72 | const MAX_MATCH = 500; 73 | 74 | // TODO (Ben @ 2023/04/04) this type definition is needed to expose internal fields, 75 | // revisit this casting once https://github.com/benrbray/noteworthy/issues/31 is resolved 76 | type InputRuleInternal = PI.InputRule & { 77 | match : RegExp; 78 | handler: (state: PS.EditorState, match: RegExpMatchArray, start: number, end: number) => PS.Transaction | null; 79 | } 80 | 81 | // unchanged from prosemirror-inputrules, except for types 82 | function run(view: PV.EditorView, from: number, to: number, text: string, rules: readonly PI.InputRule[], plugin: PS.Plugin) { 83 | if (view.composing) return false 84 | let state = view.state, $from = state.doc.resolve(from) 85 | if ($from.parent.type.spec.code) return false 86 | let textBefore = 87 | $from.parent.textBetween( 88 | Math.max(0, $from.parentOffset - MAX_MATCH), 89 | $from.parentOffset, 90 | undefined, 91 | "\ufffc" 92 | ) + text; 93 | 94 | // expose internal fields 95 | const rulesInternal = rules as InputRuleInternal[]; 96 | 97 | for (let i = 0; i < rulesInternal.length; i++) { 98 | let match = rulesInternal[i].match.exec(textBefore) 99 | let tr = match && rulesInternal[i].handler(state, match, from - (match[0].length - text.length), to) 100 | if (!tr) continue 101 | view.dispatch(tr.setMeta(plugin, {transform: tr, from, to, text})) 102 | return true 103 | } 104 | return false 105 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/prosemirror/steps.ts: -------------------------------------------------------------------------------- 1 | // prosemirror imports 2 | import { Step, StepResult } from "prosemirror-transform"; 3 | import { Node as ProseNode, Schema as ProseSchema } from "prosemirror-model"; 4 | 5 | export class SetDocAttrStep extends Step { 6 | 7 | prevValue?: T; 8 | 9 | /** 10 | * A Step representing a change to the attrs of a ProseMirror document. 11 | * As of (7/25/20), This is not possible without a custom Step. See: 12 | * https://discuss.prosemirror.net/t/changing-doc-attrs/784 13 | */ 14 | constructor(public key: string, public value: T, public stepType: string = 'SetDocAttr') { 15 | super(); 16 | } 17 | 18 | apply(doc:ProseNode) { 19 | this.prevValue = doc.attrs[this.key]; 20 | /** @todo (7/26/19) re-apply this fix if defaultAttrs needed */ 21 | //if (doc.attrs == doc.type.defaultAttrs) doc.attrs = Object.assign({}, doc.attrs); 22 | 23 | // TODO (Ben @ 2023/04/30) doc.attrs is read-only, create new doc instead 24 | // @ts-ignore 25 | doc.attrs[this.key] = this.value; 26 | 27 | return StepResult.ok(doc); 28 | } 29 | 30 | invert() { 31 | return new SetDocAttrStep(this.key, this.prevValue, 'revertSetDocAttr'); 32 | } 33 | 34 | map() { 35 | /** @todo (7/26/20) is returning null the desired behavior? */ 36 | return null; 37 | } 38 | 39 | toJSON() { 40 | return { 41 | stepType: this.stepType, 42 | key: this.key, 43 | value: this.value, 44 | }; 45 | } 46 | 47 | static fromJSON(schema:ProseSchema, json:{ key : string, value: T, stepType: string }) { 48 | return new SetDocAttrStep(json.key, json.value, json.stepType); 49 | } 50 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/prosemirror/util/mark-utils.ts: -------------------------------------------------------------------------------- 1 | // prosemirror imports 2 | import { EditorState } from "prosemirror-state"; 3 | import { MarkType, Node as ProseNode } from "prosemirror-model"; 4 | import { SelectionRange, TextSelection } from "prosemirror-state"; 5 | import { InputRule } from "prosemirror-inputrules"; 6 | 7 | //////////////////////////////////////////////////////////// 8 | 9 | export function markActive(state:EditorState, type:MarkType) { 10 | let { from, $from, to, empty } = state.selection 11 | if (empty) return type.isInSet(state.storedMarks || $from.marks()) 12 | else return state.doc.rangeHasMark(from, to, type) 13 | } 14 | 15 | export function markApplies(doc:ProseNode, ranges:SelectionRange[], type:MarkType):boolean { 16 | for (let i = 0; i < ranges.length; i++) { 17 | let { $from, $to } = ranges[i] 18 | let can = $from.depth == 0 ? doc.type.allowsMarkType(type) : false 19 | doc.nodesBetween($from.pos, $to.pos, node => { 20 | if (can) return false 21 | can = node.inlineContent && node.type.allowsMarkType(type) 22 | return true; 23 | }) 24 | if (can) return true 25 | } 26 | return false 27 | } 28 | 29 | export function markInputRule(pattern: RegExp, markType: MarkType, getAttrs?: (match: string[]) => any) { 30 | return new InputRule(pattern, (state, match, start, end) => { 31 | console.log(match, start, end); 32 | // only apply marks to non-empty text selections 33 | if (!(state.selection instanceof TextSelection)){ return null; } 34 | 35 | // determine if mark applies to match 36 | let $start = state.doc.resolve(start); 37 | let $end = state.doc.resolve(end); 38 | let range = [new SelectionRange($start, $end)]; 39 | if(!markApplies(state.doc, range, markType)){ return null; } 40 | 41 | // apply mark 42 | let tr = state.tr.replaceWith(start, end, markType.schema.text(match[1],)); 43 | return tr.addMark( 44 | tr.mapping.map(start), 45 | tr.mapping.map(end), 46 | markType.create(getAttrs ? getAttrs(match) : null) 47 | ).removeStoredMark(markType).insertText(match[2]); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/settings.ts: -------------------------------------------------------------------------------- 1 | // electron imports 2 | import Store from 'electron-store'; 3 | import { darkMode } from 'electron-util'; 4 | 5 | // project imports 6 | import { ThemeId } from '@main/theme/theme-service'; 7 | 8 | //////////////////////////////////////////////////////////// 9 | 10 | /** @todo (9/12/20) dark mode (see `darkMode.isEnabled`) */ 11 | export type NoteworthySettings = { 12 | theme: ThemeId 13 | } 14 | 15 | export const Settings = new Store({ 16 | name: '.noteworthy', 17 | defaults: { 18 | theme: { type: "default", id: "default-light" } 19 | } 20 | }); -------------------------------------------------------------------------------- /noteworthy-electron/src/common/types.ts: -------------------------------------------------------------------------------- 1 | import { Node, Schema, NodeType, MarkType, Mark } from "prosemirror-model"; 2 | import { Command } from "prosemirror-state"; 3 | 4 | declare module "prosemirror-model" { 5 | interface Fragment { 6 | // TODO (Ben @ 2023/04/30) stop relying on undocumented field, find workaround 7 | // as of (3/31/20) official @types/prosemirror-model 8 | // was missing Fragment.content, so we define it here 9 | content: Node[]; 10 | } 11 | } 12 | 13 | export interface IDisposable { 14 | dispose():void; 15 | } 16 | 17 | //////////////////////////////////////////////////////////// 18 | 19 | // --------------------------------------------------------- 20 | // TODO (2021-05-17) these are unused, remove them? 21 | // https://github.com/microsoft/TypeScript/issues/27995#issuecomment-441157546 22 | 23 | export type ArrayKeys = keyof any[]; 24 | export type Indices = Exclude; 25 | 26 | //// PROSEMIRROR /////////////////////////////////////////// 27 | 28 | // ---- prosemirror-model ---------------------------------- 29 | 30 | /** Like { NodeType } from "prosemirror-model", but more precise. */ 31 | export interface ProseNodeType< 32 | N extends string = string 33 | > extends NodeType { 34 | name: N 35 | } 36 | 37 | /** Like { MarkType } from "prosemirror-model", but more precise. */ 38 | export interface ProseMarkType< 39 | M extends string = string 40 | > extends MarkType { 41 | name: M 42 | } 43 | 44 | export type ProseKeymap = {[key: string]: Command } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/DefaultMap.ts: -------------------------------------------------------------------------------- 1 | export class DefaultMap extends Map{ 2 | 3 | defaultFn: (key: K) => V; 4 | 5 | constructor(defaultFn: (key: K) => V, iterable?: Iterable) { 6 | // map constructor 7 | if(iterable) { super(iterable); } else { super(); } 8 | // default 9 | this.defaultFn = defaultFn; 10 | } 11 | 12 | get(key:K):V { 13 | let val:V|undefined = super.get(key); 14 | if(val === undefined){ 15 | val = this.defaultFn(key); 16 | super.set(key, val); 17 | } 18 | return val; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/date.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseCslDate, format as formatCslDate, DateCsl, DateCslInvalid, DateCslValid } from "@citation-js/date"; 2 | 3 | export interface DateParts { 4 | year: number, 5 | month?: number|undefined, 6 | day?: number|undefined 7 | } 8 | 9 | function isRawDate(date: DateCsl): date is DateCslInvalid { 10 | return "raw" in date; 11 | } 12 | 13 | function isDateParts(date: DateCsl): date is DateCslValid { 14 | return "date-parts" in date; 15 | } 16 | 17 | 18 | export function parseDate(date: string): DateParts | null { 19 | const dateCsl: any = parseCslDate(date); 20 | 21 | if(isDateParts(dateCsl)) { 22 | const [year,month,day] = dateCsl["date-parts"][0]; 23 | return { year, month, day }; 24 | } else { 25 | // do not attempt to fix invalid dates 26 | // TODO (2022/03/07) accept a wider range of date formats 27 | return null; 28 | } 29 | } 30 | 31 | export function formatDate(date: DateParts): string { 32 | const { year, month, day } = date; 33 | 34 | let dateParts: [number]|[number,number]|[number,number,number]; 35 | if(month === undefined) { dateParts = [year]; } 36 | else if(day === undefined) { dateParts = [year, month]; } 37 | else { dateParts = [year]; } 38 | 39 | return formatCslDate({ "date-parts" : [dateParts] }); 40 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/equal.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/dashed/shallowequal/blob/master/index.js 2 | export function shallowEqual( 3 | objA:T, objB:T, 4 | compareObjects?:(a:T, b:T) => boolean, 5 | compareValues?: (aVal:any, bVal:any, key:keyof T)=>boolean, 6 | compareContext?:unknown 7 | ) { 8 | // compare function 9 | var ret = compareObjects ? compareObjects.call(compareContext, objA, objB) : undefined; 10 | if (ret !== undefined) { return !!ret; } 11 | 12 | // check existence 13 | if (objA === objB) { return true; } 14 | if (!objA || typeof objA !== "object") { return false; } 15 | if (!objB || typeof objB !== "object") { return false; } 16 | 17 | // compare keys 18 | var keysA:string[] = Object.keys(objA); 19 | var keysB:string[] = Object.keys(objB); 20 | if (keysA.length !== keysB.length) { return false; } 21 | 22 | var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB); 23 | 24 | // Test for A's keys different from B. 25 | for (var idx = 0; idx < keysA.length; idx++) { 26 | var key:string = keysA[idx]; 27 | 28 | if (!bHasOwnProperty(key)) { return false; } 29 | 30 | var valueA:any = objA[key]; 31 | var valueB:any = objB[key]; 32 | 33 | ret = compareValues ? compareValues.call(compareContext, valueA, valueB, key) : undefined; 34 | if (ret === false || (ret === undefined && valueA !== valueB)) { 35 | return false; 36 | } 37 | } 38 | 39 | return true; 40 | }; -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * BEGIN HEADER 3 | * 4 | * Contains: Utility function 5 | * CVM-Role: 6 | * Maintainer: Hendrik Erz 7 | * License: GNU GPL v3 8 | * 9 | * Description: This file contains a utility function to hash strings. 10 | * 11 | * END HEADER 12 | */ 13 | 14 | import { FileHash } from "@common/files" 15 | import * as pathlib from "path"; 16 | 17 | /** @todo (6/27/20) 18 | * What about multiple strings referring to the same path? 19 | * docs/file.txt vs docs/../docs/file.txt 20 | * docs/file.txt vs docs\file.txt 21 | * Will using path.normalize() fix most of these issues? 22 | */ 23 | 24 | /** 25 | * Basic hashing function (thanks to https://stackoverflow.com/a/52171480/1444650) 26 | * @param str The string that should be hashed 27 | * @return hash of the given string 28 | */ 29 | export default function hash(str:string, seed:number = 0):FileHash { 30 | let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; 31 | for (let i = 0, ch; i < str.length; i++) { 32 | ch = str.charCodeAt(i); 33 | h1 = Math.imul(h1 ^ ch, 2654435761); 34 | h2 = Math.imul(h2 ^ ch, 1597334677); 35 | } 36 | h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909); 37 | h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909); 38 | return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); 39 | }; 40 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/ignore-dir.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * BEGIN HEADER 3 | * 4 | * Contains: Utility function 5 | * CVM-Role: 6 | * Maintainer: Hendrik Erz 7 | * License: GNU GPL v3 8 | * 9 | * Description: This file contains a utility function to check for ignored dirs. 10 | * 11 | * END HEADER 12 | */ 13 | 14 | import path from "path"; 15 | 16 | /** @todo (6/28/20) load ignored dirs from file? */ 17 | // Ignored directory patterns 18 | const ignoreDirs:string[] = ['\.noteworthy'];//require('../data.json').ignoreDirs 19 | 20 | /** 21 | * Returns true, if a directory should be ignored, and false, if not. 22 | * @param {String} p The path to the directory. It will be checked against some regexps. 23 | * @return {Boolean} True or false, depending on whether or not the dir should be ignored. 24 | */ 25 | export function ignoreDir(p:string):boolean { 26 | let name = path.basename(p) 27 | // Directories are ignored on a regexp basis 28 | for (let re of ignoreDirs) { 29 | let regexp = new RegExp(re, 'i') 30 | if (regexp.test(name)) { 31 | return true 32 | } 33 | } 34 | 35 | return false 36 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/ignore-file.ts: -------------------------------------------------------------------------------- 1 | // node imports 2 | import pathlib from "path"; 3 | 4 | // Supported filetypes 5 | /** @todo (6/28/20) read supported file types from config file? */ 6 | const filetypes = [".md", ".txt", ".ipynb", ".journal", ".nwt", ".bib"]; 7 | 8 | /** 9 | * Returns true, if a given file should be ignored. 10 | * @param p The path to the file. 11 | * @return Whether the file should be ignored. 12 | */ 13 | export function ignoreFile(p:string):boolean { 14 | let ext = pathlib.extname(p).toLowerCase() 15 | return (!filetypes.includes(ext)) 16 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/is-dir.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * BEGIN HEADER 3 | * 4 | * Contains: Utility function 5 | * CVM-Role: 6 | * Maintainer: Hendrik Erz 7 | * License: GNU GPL v3 8 | * 9 | * Description: This file contains a utility function to check a path. 10 | * 11 | * END HEADER 12 | */ 13 | 14 | import fs from "fs"; 15 | 16 | /** 17 | * Checks if a given path is a valid directory 18 | * @param p The path to check 19 | * @return True, if p is valid and also a directory 20 | */ 21 | export default function isDir(p:string) { 22 | try { 23 | let s = fs.lstatSync(p); 24 | return s.isDirectory(); 25 | } catch (e) { 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/is-file.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * BEGIN HEADER 3 | * 4 | * Contains: Utility function 5 | * CVM-Role: 6 | * Maintainer: Hendrik Erz 7 | * License: GNU GPL v3 8 | * 9 | * Description: This file contains a utility function to check a file. 10 | * 11 | * END HEADER 12 | */ 13 | 14 | import fs from "fs"; 15 | 16 | /** 17 | * Checks if a given path is a valid file 18 | * @param p The path to check 19 | * @return True, if it is a valid path + file, and false if not 20 | */ 21 | export default function isFile(p:string) { 22 | try { 23 | let s = fs.lstatSync(p); 24 | return s.isFile(); 25 | } catch (e) { 26 | return false; 27 | } 28 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/non-void.ts: -------------------------------------------------------------------------------- 1 | export function filterNonVoid(array:(T|null|undefined)[]): T[] { 2 | return array.filter(isNonVoid); 3 | } 4 | 5 | export function isNonVoid(value : T | null | undefined): value is T { 6 | return value !== null && value !== undefined; 7 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/pathstrings.ts: -------------------------------------------------------------------------------- 1 | export function replaceInvalidFileNameChars(fileName:string){ 2 | console.log("before", fileName); 3 | let after = fileName.trim() 4 | .replace(/[:\/\\<>\?\*"']/g, "_") 5 | .replace(/\s/g, " "); 6 | console.log("after", after); 7 | return after; 8 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/pick.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return a new object with only the specified keys (non-inclusive, meaning 3 | * that non-existent keys will not be present in the returned object). 4 | * @see https://stackoverflow.com/a/56592365 5 | */ 6 | export function pick(obj: T, keys: K[]) { 7 | return Object.fromEntries( 8 | keys 9 | .filter(key => key in obj) // remove this line to make inclusive 10 | .map(key => [key, obj[key]]) 11 | ) 12 | }; -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/random.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | /** @todo (7/12/20) speed test? might be really slow */ 4 | export function randomId():string { 5 | let result = crypto.randomBytes(4); 6 | return result.readUInt32LE(0).toString() 7 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/common/util/to.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * async await wrapper for easy error handling like so: 3 | * 4 | * let [err,result] = await to(otherAsyncFunction()); 5 | * if(err){ handle error } 6 | * 7 | * @see https://github.com/scopsy/await-to-js 8 | * @see https://stackoverflow.com/a/53689892/1444650 9 | * @param promise 10 | * @param errorExt Additional Information you can pass to the err object 11 | */ 12 | export async function to( 13 | promise: Promise, 14 | ): Promise<[U,undefined]|[null, T]> { 15 | return promise 16 | .then<[null, T]>((data: T) => [null, data]) 17 | .catch<[U, undefined]>((err: U) => { 18 | // attach extra error information 19 | return [err, undefined]; 20 | }); 21 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/README.md: -------------------------------------------------------------------------------- 1 | # `noteworthy-extensions` 2 | 3 | I aim to make Noteworthy's core as small as possible, with an extension system flexible enough that community-made extensions can dramatically alter the behavior of the editor. At the moment, I'm working on extracting as many features as possible out of the kernel and into standalone extensions. The design of the Noteworthy extension system has been inspired by: 4 | 5 | * Visual Studio Code [Extension API](https://code.visualstudio.com/api) and [Contribution Points](https://code.visualstudio.com/api/references/contribution-points) 6 | * Remirror [Extensions](https://remirror.io/docs/concepts/extension) 7 | * Obsidian [Plugins](https://marcus.se.net/obsidian-plugin-docs/) -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-autocomplete/README.md: -------------------------------------------------------------------------------- 1 | # `noteworthy-autocomplete` 2 | 3 | An autocomplete popup box, based on `prosemirror-autocomplete`. 4 | 5 | ## Preview 6 | 7 | ![noteworthy-autocomplete](img/noteworthy-autocomplete.gif) 8 | -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-autocomplete/autocomplete-component.tsx: -------------------------------------------------------------------------------- 1 | // solid 2 | import * as S from "solid-js"; 3 | import { For, Show, Match, Switch } from 'solid-js/web'; 4 | import { SuggestDataWithIdx, SuggestItemFancy, SuggestItemSimple } from "./autocomplete-extension"; 5 | 6 | //////////////////////////////////////////////////////////// 7 | 8 | const renderFancyLabel = (fancy: SuggestItemFancy) => { 9 | return (
10 | 11 | {(a) => { 12 | return ({a.text}) 13 | }} 14 | 15 |
); 16 | } 17 | 18 | export const Suggest = ( 19 | props: { 20 | open: boolean, 21 | selectedIdx: number, 22 | pos: {top:number, left:number}, 23 | data: SuggestDataWithIdx, 24 | onItemHover: (idx: number, evt: MouseEvent) => void, 25 | onItemClick: (idx: number, evt: MouseEvent) => void 26 | } 27 | ) => { 28 | // mapping from group -> starting index 29 | const groupIdxMap = S.createMemo(() => { 30 | const result = props.data.map(g => g.items.length); 31 | for(let k = 0, len = 0; k < result.length; k++) { 32 | let groupLen = result[k]; 33 | result[k] = len; 34 | len += groupLen; 35 | } 36 | return result; 37 | }); 38 | 39 | const classList = () => { 40 | const isOpen = props.open; 41 | return { 42 | "suggest" : true, 43 | "suggest-open" : isOpen, 44 | "suggest-closed" : !isOpen 45 | } 46 | } 47 | 48 | const handleHover = (idx: number) => (evt: MouseEvent) => { 49 | props.onItemHover(idx, evt); 50 | } 51 | 52 | const handleClick = (idx: number) => (evt: MouseEvent) => { 53 | props.onItemClick(idx, evt); 54 | } 55 | 56 | return (
60 | 0} 62 | fallback={ 63 |
64 |
No Results
65 |
} 66 | > 67 | 68 | {(group, idxGrp) => { 69 | return ( 70 |
71 |
{group.label}
72 |
73 | 74 | {(item, idxItem) => { 75 | // compute idx in flattened group hierarchy 76 | const itemIdx = () => { return groupIdxMap()[idxGrp()] + idxItem() }; 77 | const classList = () => { 78 | return { 79 | "suggest-item" : true, 80 | "selected" : props.selectedIdx === itemIdx() 81 | } 82 | } 83 | 84 | return ( 85 |
90 | 91 | 92 | { (item as SuggestItemSimple).text } 93 | 94 | 95 | {renderFancyLabel(item as SuggestItemFancy)} 96 | 97 | 98 |
99 | ); 100 | }} 101 |
102 |
103 |
104 | )}} 105 |
106 |
107 |
); 108 | } 109 | -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-autocomplete/autocomplete.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --suggest-padding-h: 0.6rem; 3 | --suggest-padding-v: 0.6rem; 4 | } 5 | 6 | #suggest-wrapper { 7 | position: absolute; 8 | right: 0; 9 | left: 0; 10 | } 11 | 12 | .suggest { 13 | position: absolute; 14 | 15 | font-size: 1.0em; 16 | font-family: var(--font-ui); 17 | 18 | border-radius: 4px; 19 | box-shadow: inset; 20 | background-color: white; 21 | 22 | min-width: 10em; 23 | max-height: 30vh; 24 | overflow-y: scroll; 25 | overflow-x: hidden; 26 | 27 | user-select: none; 28 | z-index: 1000; 29 | 30 | box-sizing: border-box; 31 | 32 | box-shadow: 33 | rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, 34 | rgba(15, 15, 15, 0.1) 0px 4px 8px; 35 | } 36 | 37 | .suggest.suggest-open { 38 | display: block; 39 | } 40 | 41 | .suggest.suggest-closed { 42 | display: none; 43 | } 44 | 45 | .suggest-group-label { 46 | margin: 0.5em 0; 47 | padding: 0 var(--suggest-padding-h); 48 | color: var(--font-color-ui-faint); 49 | font-size: 0.8em; 50 | } 51 | 52 | .suggest-item { 53 | padding: 0.32rem var(--suggest-padding-h); 54 | font-size: 14px; 55 | } 56 | 57 | .suggest-item.selected { 58 | background-color: var(--color-primary-bg-2); 59 | color: var(--font-color-ui-default); 60 | } 61 | 62 | .autocomplete { 63 | background-color: var(--color-bg-neutral); 64 | } 65 | 66 | .suggest-item .suggest-label-code { 67 | font-family: var(--code-font); 68 | background-color: var(--color-primary-bg-2); 69 | padding: 0.1em 0.2em; 70 | border: 1px solid var(--color-primary-bg-4); 71 | } 72 | 73 | .suggest-item .suggest-fuzzy-match { 74 | font-weight: bold; 75 | } 76 | -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-autocomplete/img/noteworthy-autocomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/extensions/noteworthy-autocomplete/img/noteworthy-autocomplete.gif -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-autocomplete/index.ts: -------------------------------------------------------------------------------- 1 | import { NoteworthyExtensionInitializer } from "@common/extensions/noteworthy-extension"; 2 | import AutocompleteExtension, { Autocomplete, AutocompleteProviders, SuggestData, extensionSpec } from "./autocomplete-extension"; 3 | 4 | /* TODO (Ben @ 2023/05/01) this extension is part of noteworthy, so css can be 5 | * embedded automatically during the build process. Community extensions won't 6 | * be able to do this, so the plugin spec should specify CSS file paths to be 7 | * loaded dynamically at initialization time. (and perhaps watched for changes) 8 | */ 9 | import "./autocomplete.css" 10 | import { NoteworthyExtensionApi } from "@common/extensions/extension-api"; 11 | 12 | export const autocompleteExtension: NoteworthyExtensionInitializer = { 13 | spec: extensionSpec, 14 | initialize({ editorElt, api }) { 15 | return new AutocompleteExtension(editorElt, makeProviders(api)); 16 | } 17 | } 18 | 19 | export default autocompleteExtension; 20 | 21 | //////////////////////////////////////////////////////////////////////////////// 22 | 23 | const makeProviders = (api: NoteworthyExtensionApi): AutocompleteProviders => ({ 24 | wikilink : { 25 | startTrigger: "[[", 26 | endTrigger: "]]", 27 | allowSpace: false, 28 | search: async (query: string) => { 29 | const result = await api.fuzzyTagSearch(query); 30 | 31 | if(result.length === 0) { return []; } 32 | 33 | const data: SuggestData = [{ 34 | label: "Tag Search", 35 | items: result.map(tag => ({ 36 | kind: "fancy", 37 | id: tag.result, 38 | text: tag.result, 39 | dom: tag.resultEmphasized.map(part => ({ text: part.text, class: part.emph ? "suggest-fuzzy-match" : ""})) 40 | })) 41 | }]; 42 | 43 | return data; 44 | }, 45 | accept: (id, view, range) => { 46 | const tr = view.state.tr 47 | .deleteRange(range.from, range.to) 48 | .insertText(`[[${id}]]`); 49 | view.dispatch(tr); 50 | view.focus(); 51 | } 52 | }, 53 | citation : { 54 | startTrigger: "@[", 55 | endTrigger: "]", 56 | allowSpace: false, 57 | search: async (query: string) => { 58 | const result = await api.fuzzyTagSearch(query); 59 | 60 | if(result.length === 0) { return []; } 61 | 62 | const data: SuggestData = [{ 63 | label: "Citation Search", 64 | items: result.map(tag => ({ 65 | kind: "fancy", 66 | id: tag.result, 67 | text: tag.result, 68 | dom: tag.resultEmphasized.map(part => ({ text: part.text, class: part.emph ? "suggest-fuzzy-match" : ""})) 69 | })) 70 | }]; 71 | 72 | return data; 73 | }, 74 | accept: (id, view, range) => { 75 | const tr = view.state.tr 76 | .deleteRange(range.from, range.to) 77 | .insertText(`@[${id}]`); 78 | view.dispatch(tr); 79 | view.focus(); 80 | } 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-codemirror-preview/codemirror-preview-extension.ts: -------------------------------------------------------------------------------- 1 | // prosemirror 2 | import * as PS from "prosemirror-state"; 3 | 4 | // noteworthy-codemirror-preview 5 | import { NoteworthyExtension, NoteworthyExtensionSpec } from "@common/extensions/noteworthy-extension"; 6 | import { codeMirrorPlugin as codeMirrorPreviewPlugin } from "./codemirror-preview-plugin"; 7 | import { PreviewRenderers } from "./codemirror-preview-types"; 8 | 9 | //////////////////////////////////////////////////////////////////////////////// 10 | 11 | export namespace CodeMirrorPreview { 12 | export type Name = "codeMirrorPreview"; 13 | 14 | export interface Config { 15 | previewRenderers: PreviewRenderers 16 | } 17 | } 18 | 19 | 20 | //////////////////////////////////////////////////////////////////////////////// 21 | 22 | // register the extension with Noteworthy 23 | declare module "@common/extensions/noteworthy-extension" { 24 | export interface CommunityExtensions { 25 | codeMirrorPreview: { 26 | config: CodeMirrorPreview.Config 27 | } 28 | } 29 | } 30 | 31 | //////////////////////////////////////////////////////////////////////////////// 32 | 33 | export const codeMirrorPreviewSpec: NoteworthyExtensionSpec = { 34 | name : "codeMirrorPreview", 35 | } 36 | 37 | export default class CodeMirrorPreviewExtension 38 | extends NoteworthyExtension { 39 | 40 | private _previewRenderers: PreviewRenderers = {}; 41 | private _codeMirrorPreviewPlugin: PS.Plugin|null = null; 42 | 43 | constructor(){ 44 | super(); 45 | } 46 | 47 | override updateConfig(updated: CodeMirrorPreview.Config): void { 48 | // override language preview renderers with updated values 49 | Object.assign(this._previewRenderers, updated.previewRenderers); 50 | } 51 | 52 | override makeProseMirrorPlugins(): PS.Plugin[] { 53 | // initialize preview plugin 54 | this._codeMirrorPreviewPlugin = codeMirrorPreviewPlugin({ 55 | mode: "preview", 56 | previewRenderers: this._previewRenderers 57 | }); 58 | 59 | return [this._codeMirrorPreviewPlugin]; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-codemirror-preview/codemirror-preview-plugin.ts: -------------------------------------------------------------------------------- 1 | // prosemirror 2 | import * as PM from "prosemirror-model"; 3 | import * as PS from "prosemirror-state"; 4 | import * as PV from "prosemirror-view"; 5 | 6 | // noteworthy-codemirror-preview 7 | import { CodeMirrorView, CodeViewOptions } from "./codemirror-preview-nodeview"; 8 | import { PreviewRenderers } from "./codemirror-preview-types"; 9 | 10 | //////////////////////////////////////////////////////////////////////////////// 11 | 12 | namespace CodeMirrorPlugin { 13 | 14 | export type Options = CodeViewOptions 15 | 16 | export interface State { 17 | // empty 18 | } 19 | 20 | } 21 | 22 | let codeMirrorPluginKey = new PS.PluginKey("noteworthy-codemirror"); 23 | 24 | export const codeMirrorPlugin = (options: CodeMirrorPlugin.Options): PS.Plugin => { 25 | let pluginSpec: PS.PluginSpec = { 26 | key: codeMirrorPluginKey, 27 | state: { 28 | init(config, instance): CodeMirrorPlugin.State { 29 | return { }; 30 | }, 31 | apply(tr, value, oldState, newState){ 32 | return value; 33 | }, 34 | }, 35 | props: { 36 | nodeViews: { 37 | "code_block" : (node: PM.Node, view: PV.EditorView, getPos: ()=>(number|undefined)): CodeMirrorView => { 38 | return new CodeMirrorView( 39 | node, 40 | view, 41 | getPos as (() => number), 42 | options 43 | ); 44 | } 45 | } 46 | } 47 | } 48 | 49 | return new PS.Plugin(pluginSpec); 50 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-codemirror-preview/codemirror-preview-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Function which updates the DOM element with a preview of `code`. 3 | * 4 | * @returns `true` if the preview rendered successfully, `false` otherwise. Can 5 | * be used to report errors or disable the preview based on the contents of `code`. 6 | */ 7 | export type PreviewRenderer = (dom: HTMLElement, code: string) => boolean; 8 | 9 | export type PreviewRenderers = { [lang:string] : PreviewRenderer }; -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-codemirror-preview/codemirror-utils.ts: -------------------------------------------------------------------------------- 1 | // codemirror 2 | import * as CL from "@codemirror/language" 3 | import * as CJS from "@codemirror/lang-javascript" 4 | import { cppLanguage } from "@codemirror/lang-cpp" 5 | import { pythonLanguage } from "@codemirror/lang-python" 6 | import { javaLanguage } from "@codemirror/lang-java" 7 | import { jsonLanguage } from "@codemirror/lang-json" 8 | 9 | // codemirror legacy languages 10 | import {haskell} from "@codemirror/legacy-modes/mode/haskell" 11 | import {c, scala} from "@codemirror/legacy-modes/mode/clike" 12 | import {lua} from "@codemirror/legacy-modes/mode/lua" 13 | import {julia} from "@codemirror/legacy-modes/mode/julia" 14 | import {yaml} from "@codemirror/legacy-modes/mode/yaml" 15 | 16 | //////////////////////////////////////////////////////////////////////////////// 17 | 18 | export function getCodeMirrorLanguage(lang: string|null): CL.Language|null { 19 | // javascript / typescript 20 | if(lang === "javascript") { return CJS.javascriptLanguage; } 21 | if(lang === "js") { return CJS.javascriptLanguage; } 22 | if(lang === "jsx") { return CJS.jsxLanguage; } 23 | if(lang === "typescript") { return CJS.typescriptLanguage; } 24 | if(lang === "js") { return CJS.typescriptLanguage; } 25 | if(lang === "tsx") { return CJS.tsxLanguage; } 26 | // clike 27 | if(lang === "c") { return CL.StreamLanguage.define(c); } 28 | if(lang === "cpp") { return cppLanguage; } 29 | if(lang === "c++") { return cppLanguage; } 30 | if(lang === "java") { return javaLanguage; } 31 | if(lang === "scala") { return CL.StreamLanguage.define(scala); } 32 | // scientific 33 | if(lang === "julia") { return CL.StreamLanguage.define(julia); } 34 | if(lang === "lua") { return CL.StreamLanguage.define(lua); } 35 | if(lang === "python") { return pythonLanguage; } 36 | // functional 37 | if(lang === "haskell") { return CL.StreamLanguage.define(haskell); } 38 | // config 39 | if(lang === "json") { jsonLanguage; } 40 | if(lang === "yaml") { return CL.StreamLanguage.define(yaml); } 41 | // other 42 | if(lang === "yaml") { return CL.StreamLanguage.define(yaml); } 43 | 44 | // default 45 | return null; 46 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-codemirror-preview/index.ts: -------------------------------------------------------------------------------- 1 | import { NoteworthyExtensionInitializer } from "@common/extensions/noteworthy-extension"; 2 | import CodeMirrorPreviewExtension, { CodeMirrorPreview, codeMirrorPreviewSpec } from "./codemirror-preview-extension"; 3 | 4 | export const codeMirrorPreviewExtension: NoteworthyExtensionInitializer = { 5 | spec: codeMirrorPreviewSpec, 6 | initialize() { 7 | return new CodeMirrorPreviewExtension(); 8 | } 9 | } 10 | 11 | export default codeMirrorPreviewExtension; 12 | -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-seed/index.ts: -------------------------------------------------------------------------------- 1 | import { NoteworthyExtensionInitializer } from "@common/extensions/noteworthy-extension"; 2 | import SeedExtension, { Seed, seedExtensionSpec } from "./seed-extension"; 3 | 4 | //////////////////////////////////////////////////////////////////////////////// 5 | 6 | declare module "@common/extensions/noteworthy-extension" { 7 | export interface CommunityExtensionCommands { 8 | seedNew: { seedName : string } 9 | } 10 | } 11 | 12 | //////////////////////////////////////////////////////////////////////////////// 13 | 14 | export const seedExtension: NoteworthyExtensionInitializer< 15 | Seed.Name, // extension name 16 | [] // dependencies 17 | > = { 18 | spec: seedExtensionSpec, 19 | 20 | initialize({ api }) { 21 | // api.registerCommand("seedNew", async ({ seedName }) => { 22 | // console.log(`create seed (name: ${seedName})`); 23 | // }); 24 | 25 | return new SeedExtension(); 26 | } 27 | } 28 | 29 | export default seedExtension; 30 | -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-seed/seed-extension.ts: -------------------------------------------------------------------------------- 1 | // noteworthy 2 | import { NoteworthyExtension, NoteworthyExtensionSpec } from "@common/extensions/noteworthy-extension"; 3 | 4 | export const seedExtensionSpec: NoteworthyExtensionSpec< 5 | "seed", 6 | [] 7 | > = { 8 | name: "seed", 9 | } 10 | 11 | //////////////////////////////////////////////////////////////////////////////// 12 | 13 | export namespace Seed { 14 | export type Name = "seed"; 15 | export interface Config { }; 16 | } 17 | 18 | declare module "@common/extensions/noteworthy-extension" { 19 | export interface CommunityExtensions { 20 | seed: { 21 | config: Seed.Config 22 | } 23 | } 24 | } 25 | 26 | export default class SeedExtension extends NoteworthyExtension { 27 | 28 | private _seeds: { 29 | name: string, 30 | seed: SeedDoc 31 | }[]; 32 | 33 | constructor() { 34 | super(); 35 | 36 | this._seeds = []; 37 | } 38 | 39 | updateConfig(updated: Seed.Config): void { 40 | // extension has no config, do nothing 41 | } 42 | } 43 | 44 | 45 | //////////////////////////////////////////////////////////////////////////////// 46 | 47 | export interface SeedDoc { 48 | 49 | } 50 | -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-tikzjax/index.ts: -------------------------------------------------------------------------------- 1 | import { NoteworthyExtensionInitializer } from "@common/extensions/noteworthy-extension"; 2 | import TizkJaxExtension, { TikzJax, tikzJaxExtensionSpec } from "./tikzjax-extension"; 3 | 4 | export const tikzJaxExtension: NoteworthyExtensionInitializer< 5 | TikzJax.Name, // extension name 6 | ["codeMirrorPreview"] // dependencies 7 | > = { 8 | spec: tikzJaxExtensionSpec, 9 | initialize() { 10 | return new TizkJaxExtension(); 11 | } 12 | } 13 | 14 | export default tikzJaxExtension; 15 | -------------------------------------------------------------------------------- /noteworthy-electron/src/extensions/noteworthy-tikzjax/tikzjax-extension.ts: -------------------------------------------------------------------------------- 1 | // noteworthy 2 | import { NoteworthyExtension, NoteworthyExtensionSpec } from "@common/extensions/noteworthy-extension"; 3 | 4 | // other extensions 5 | import { PreviewRenderer } from "../noteworthy-codemirror-preview/codemirror-preview-types"; 6 | 7 | //////////////////////////////////////////////////////////////////////////////// 8 | 9 | // https://vitejs.dev/guide/assets.html#importing-asset-as-string 10 | import tikzJaxSource from "./lib/tikzjax/tikzjax.js?raw" 11 | import "./lib/tikzjax/tikzjax.css" 12 | 13 | function loadTikzJax(doc: Document) { 14 | const s = document.createElement("script"); 15 | s.id = "tikzjax"; 16 | s.type = "text/javascript"; 17 | s.innerHTML = tikzJaxSource; 18 | doc.body.appendChild(s); 19 | } 20 | 21 | window.addEventListener("load", evt => { 22 | loadTikzJax(window.document); 23 | }); 24 | 25 | //////////////////////////////////////////////////////////////////////////////// 26 | 27 | function stripEmptyLines(s: string): string { 28 | return s.replace(/^\n/gm, ""); 29 | } 30 | 31 | function makeTikzCdDocument(code: string): string { 32 | return stripEmptyLines(` 33 | \\usepackage{tikz-cd} 34 | \\begin{document} 35 | \\begin{tikzcd} 36 | ${code} 37 | \\end{tikzcd} 38 | \\end{document} 39 | `); 40 | } 41 | 42 | function makeQuiverDocument(code: string): string { 43 | return stripEmptyLines(` 44 | \\usepackage{tikz-cd} 45 | \\usepackage{quiver} 46 | \\begin{document} 47 | ${code} 48 | \\end{document} 49 | `); 50 | } 51 | 52 | export const tikzjaxRenderers: { [lang:string]: PreviewRenderer } = { 53 | "tikz" : (dom: HTMLElement, code: string): boolean => { 54 | dom.innerHTML = ``; 55 | return true; 56 | }, 57 | "tikzcd" : (dom: HTMLElement, code: string): boolean => { 58 | dom.innerHTML = ``; 59 | return true; 60 | }, 61 | "quiver" : (dom: HTMLElement, code: string): boolean => { 62 | dom.innerHTML = ``; 63 | return true; 64 | } 65 | } 66 | 67 | //////////////////////////////////////////////////////////////////////////////// 68 | 69 | export const tikzJaxExtensionSpec: NoteworthyExtensionSpec< 70 | "tikzJax", 71 | ["codeMirrorPreview"] 72 | > = { 73 | name: "tikzJax", 74 | config: { 75 | codeMirrorPreview: { 76 | previewRenderers: tikzjaxRenderers 77 | } 78 | } 79 | } 80 | 81 | //////////////////////////////////////////////////////////////////////////////// 82 | 83 | export namespace TikzJax { 84 | export type Name = "tikzJax"; 85 | export interface Config { }; 86 | } 87 | 88 | declare module "@common/extensions/noteworthy-extension" { 89 | export interface CommunityExtensions { 90 | tikzJax: { 91 | config: TikzJax.Config 92 | } 93 | } 94 | } 95 | 96 | export default class TizkJaxExtension extends NoteworthyExtension { 97 | 98 | constructor() { super(); } 99 | 100 | updateConfig(updated: TikzJax.Config): void { 101 | // extension has no config, do nothing 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/MainIPC.ts: -------------------------------------------------------------------------------- 1 | // noteworthy imports 2 | import { MainIpc_DialogHandlers } from "./ipc/dialog"; 3 | import { MainIpc_FileHandlers } from "./ipc/file"; 4 | import { MainIpc_LifecycleHandlers } from "./ipc/lifecycle"; 5 | import { MainIpc_MetadataHandlers } from "./ipc/metadata"; 6 | import { MainIpc_NavigationHandlers } from "./ipc/navigation"; 7 | import { MainIpc_OutlineHandlers } from "./ipc/outline"; 8 | import { MainIpc_ShellHandlers } from "./ipc/shell"; 9 | import { MainIpc_TagHandlers } from "./ipc/tag"; 10 | import { MainIpc_ThemeHandlers } from "./ipc/theme"; 11 | import { MainIPC_WorkspaceHandlers } from "./ipc/workspace"; 12 | 13 | //////////////////////////////////////////////////////////////////////////////// 14 | 15 | declare global { 16 | namespace Noteworthy { 17 | export interface MainIpcHandlers { 18 | // plugins can add additional handler types by 19 | // augmenting this interface with type declarations 20 | } 21 | } 22 | } 23 | 24 | export interface DefaultMainIpcHandlers { 25 | lifecycle: MainIpc_LifecycleHandlers; 26 | file: MainIpc_FileHandlers; 27 | theme: MainIpc_ThemeHandlers; 28 | shell: MainIpc_ShellHandlers; 29 | dialog: MainIpc_DialogHandlers; 30 | tag: MainIpc_TagHandlers; 31 | outline: MainIpc_OutlineHandlers; 32 | metadata: MainIpc_MetadataHandlers; 33 | navigation: MainIpc_NavigationHandlers; 34 | workspace: MainIPC_WorkspaceHandlers; 35 | }; 36 | 37 | export type MainIpcHandlers = Noteworthy.MainIpcHandlers & DefaultMainIpcHandlers 38 | export type MainIpcChannelName = keyof MainIpcHandlers; 39 | 40 | export interface MainIpcChannel { 41 | readonly name: MainIpcChannelName; 42 | } 43 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/default-index.ts: -------------------------------------------------------------------------------- 1 | import { app, shell, BrowserWindow } from 'electron' 2 | import { join } from 'path' 3 | import { electronApp, optimizer, is } from '@electron-toolkit/utils' 4 | 5 | function createWindow(): void { 6 | // Create the browser window. 7 | const mainWindow = new BrowserWindow({ 8 | width: 900, 9 | height: 670, 10 | show: false, 11 | autoHideMenuBar: true, 12 | icon: undefined, //...(process.platform === 'linux' ? { icon } : {}), 13 | webPreferences: { 14 | preload: join(__dirname, '../preload/preload.js'), 15 | sandbox: false 16 | } 17 | }) 18 | 19 | mainWindow.on('ready-to-show', () => { 20 | mainWindow.show() 21 | }) 22 | 23 | mainWindow.webContents.setWindowOpenHandler((details) => { 24 | shell.openExternal(details.url) 25 | return { action: 'deny' } 26 | }) 27 | 28 | // HMR for renderer base on electron-vite cli. 29 | // Load the remote URL for development or the local html file for production. 30 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 31 | mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) 32 | } else { 33 | mainWindow.loadFile(join(__dirname, '../renderer/index.html')) 34 | } 35 | } 36 | 37 | // This method will be called when Electron has finished 38 | // initialization and is ready to create browser windows. 39 | // Some APIs can only be used after this event occurs. 40 | app.whenReady().then(() => { 41 | // Set app user model id for windows 42 | electronApp.setAppUserModelId('com.electron') 43 | 44 | // Default open or close DevTools by F12 in development 45 | // and ignore CommandOrControl + R in production. 46 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 47 | app.on('browser-window-created', (_, window) => { 48 | optimizer.watchWindowShortcuts(window) 49 | }) 50 | 51 | createWindow() 52 | 53 | app.on('activate', function () { 54 | // On macOS it's common to re-create a window in the app when the 55 | // dock icon is clicked and there are no other windows open. 56 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 57 | }) 58 | }) 59 | 60 | // Quit when all windows are closed, except on macOS. There, it's common 61 | // for applications and their menu bar to stay active until the user quits 62 | // explicitly with Cmd + Q. 63 | app.on('window-all-closed', () => { 64 | if (process.platform !== 'darwin') { 65 | app.quit() 66 | } 67 | }) 68 | 69 | // In this file you can include the rest of your app"s specific main process 70 | // code. You can also put them in separate files and require them here. 71 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/fsal/fsal-watcher.ts: -------------------------------------------------------------------------------- 1 | import chokidar, { FSWatcher } from "chokidar"; 2 | import { EventEmitter } from "events"; 3 | import { FsalEvents, ChokidarEvents } from "@common/events"; 4 | 5 | //////////////////////////////////////////////////////////// 6 | 7 | export default class FSALWatchdog extends EventEmitter { 8 | 9 | private _process:FSWatcher|null; // chokidar process 10 | private _isBooting:boolean; 11 | private _paths:Set 12 | private _ignoreDotfiles:boolean; 13 | 14 | constructor(ignoreDotfiles:boolean=true){ 15 | super(); 16 | 17 | this._ignoreDotfiles = ignoreDotfiles; 18 | 19 | this._process = null; 20 | this._paths = new Set(); 21 | this._isBooting = false; 22 | } 23 | 24 | /** 25 | * @param p initial path to watch 26 | */ 27 | async init(p?:string){ 28 | console.log("fsal-watcher :: init() :: path=", p); 29 | 30 | // don't boot up twice, and only boot if there's at least one path 31 | if(this._paths.size < 1 || this.isBooting()){ return; } 32 | this._isBooting = true; 33 | 34 | /** @todo (9/13/20) 35 | * Had to disable .dotfile ignore because user config 36 | * folder is ~/.config/noteworth on some systems. 37 | * 38 | * Real solution requires multiple chokidar instances: 39 | * > global instance for ad hoc file watching 40 | * > per-workspace instance, 41 | * 42 | * FSAL should also have an option to emit events ONLY 43 | * when a listener-specified folder has changed. 44 | * 45 | * E.g. the ThemeService should only receive events 46 | * when the theme folder has changed! Right now it receives 47 | * all chokidar events. 48 | */ 49 | 50 | // chokidar's ignored-setting is compatible to anymatch, so we can 51 | // pass an array containing the standard dotted directory-indicators, 52 | // directories that should be ignored and a function that returns true 53 | // for all files that are _not_ in the filetypes list (whitelisting) 54 | // Further reading: https://github.com/micromatch/anymatch 55 | let ignoreDirs: (RegExp|string)[] = this._ignoreDotfiles?[ 56 | ///(^|[/\\])\../, 57 | '**/.noteworthy/**', 58 | '**/.git/**', 59 | '**/.vscode/**' 60 | ]:[]; 61 | 62 | this._process = chokidar.watch( (p?p:[]), { 63 | ignored: ignoreDirs, 64 | persistent: true, 65 | ignoreInitial: true 66 | }); 67 | 68 | // attach events only after chokidar's initial scan is complete 69 | this._process.on(ChokidarEvents.READY, (event: string, path: string) => { 70 | if(!this._process){ 71 | console.error("chokidar :: ready :: unknown startup error"); 72 | return; 73 | } 74 | // check for paths that may have been added while starting up 75 | console.log("chokidar :: ready"); 76 | let alreadyWatched = Object.keys(this._process.getWatched()); 77 | for(let p of this._paths){ 78 | if(!alreadyWatched.includes(p)){ 79 | this._process.add(p); 80 | } 81 | } 82 | // finished booting 83 | this._isBooting = false; 84 | }); 85 | 86 | // chokidar events 87 | this._process.on(ChokidarEvents.ALL, (event:string,path:string) => { 88 | this.emit(FsalEvents.CHOKIDAR_EVENT, event, { path }); 89 | }); 90 | } 91 | 92 | destroy(){ 93 | if(this._process){ 94 | this._process.removeAllListeners(); 95 | this._process.close(); 96 | this._process = null; 97 | } 98 | 99 | this._paths.clear(); 100 | this._isBooting = false; 101 | } 102 | 103 | isBooting(){ return this._isBooting; }; 104 | 105 | ignoreOnce(){ } 106 | 107 | // == Watched Paths ================================= // 108 | 109 | /** 110 | * Adds a path to the currently watched paths. 111 | * @returns self (for chainability) 112 | */ 113 | watch(p:string){ 114 | console.log("fsal-watcher :: watch ::", p); 115 | // ignore duplicate paths 116 | if (this._paths.has(p)) { console.log("\tpath already watched");return this; } 117 | // add the path 118 | this._paths.add(p); 119 | // if fsal is booting up, _paths will be watched when chokidar is ready 120 | if (this.isBooting()) { console.log("\twatcher booting"); return this; } 121 | // start the watchdog if needed 122 | if (!this._process) { console.log("\tinitializing watcher");this.init(p); } 123 | else { this._process.add(p); } 124 | // chainable 125 | return this; 126 | } 127 | 128 | /** 129 | * Removes a patch from the watchdog process. 130 | * @param p The path to unwatch. 131 | * @returns self (for chainability) 132 | */ 133 | unwatch(p:string){ 134 | console.log("fsal-watcher :: unwatch ::", p); 135 | if (!this._process) { return this; } 136 | // remove from watched paths 137 | this._paths.delete(p); 138 | this._process.unwatch(p); 139 | return this; 140 | } 141 | 142 | unwatchAll(){ 143 | for(let p in this._paths){ 144 | this.unwatch(p); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/fsal/fsal.ts: -------------------------------------------------------------------------------- 1 | // project imports 2 | import { IDirectory, IDirEntry, IDirectoryMeta, IDirEntryMeta, IFileMeta, IFileDesc } from "@common/files"; 3 | 4 | //////////////////////////////////////////////////////////////////////////////// 5 | 6 | /** 7 | * Represents anything capable of loading and saving files, 8 | * and emitting events when the corresponding files are changed. 9 | */ 10 | export interface FSAL { 11 | /** Perform any necessary initialization. */ 12 | init(): Promise; 13 | 14 | /** Perform any necessary cleanup. */ 15 | close(): Promise; 16 | 17 | /** 18 | * Return the contents of the given file, if it exists. 19 | * Otherwise, returns NULL. 20 | */ 21 | readFile(filePath: string): string | null; 22 | 23 | /** 24 | * Save the given text to the given file path. 25 | */ 26 | saveFile(filePath: string, fileText: string, mkDirs: boolean): Promise; 27 | 28 | /** 29 | * Create a new file. 30 | * TODO should this be merged with saveFile? 31 | */ 32 | createFile(path: string, contents?: string): Promise 33 | 34 | /** 35 | * Reads in a file tree recursively, returning the directory descriptor object. 36 | * 37 | * @param currentPath The current path of the directory 38 | * @param parent A parent (or null, if it's a root) 39 | */ 40 | parseDir(dirPath:string, parent?:IDirectory|null): Promise 41 | 42 | parseFile(filePath:string, parent?:IDirectory|null): Promise 43 | 44 | addListener: (event: string | symbol, listener: (...args: any[]) => void) => FSAL; 45 | 46 | /** 47 | * Watch the given path. (should belong to the workspace!) 48 | * TODO: (2021-05-30) does watch belong on FSAL? 49 | */ 50 | watch(path: string): void; 51 | 52 | /** 53 | * Watch the given global path. (need not belong to the workspace) 54 | * TODO: (2021-05-30) does watchGlobal belong on FSAL? 55 | */ 56 | watchGlobal(path: string): void; 57 | } 58 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/global.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | declare global { 4 | /* use var, not let or const, see https://stackoverflow.com/a/69429093 */ 5 | var isQuitting: boolean; 6 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/main/index.ts: -------------------------------------------------------------------------------- 1 | // electron 2 | import { app, shell, BrowserWindow, Menu } from 'electron' 3 | import { electronApp, optimizer, is } from '@electron-toolkit/utils' 4 | 5 | // node 6 | import { join } from 'path' 7 | 8 | // noteworthy 9 | import NoteworthyApp from './app'; 10 | import FSALSystem from './fsal/fsal-system'; 11 | import { WorkspaceService } from './workspace/workspace-service'; 12 | import { PluginService } from './plugins/plugin-service'; 13 | import { ThemeService } from './theme/theme-service'; 14 | import { makeAppMenuTemplate } from "./menus/app-menu"; 15 | 16 | // assets 17 | import icon from '../../resources/icon.png?asset' 18 | 19 | //////////////////////////////////////////////////////////////////////////////// 20 | 21 | async function createAppMenu(app: NoteworthyApp, themeService: ThemeService) { 22 | const appMenuTemplate = await makeAppMenuTemplate(app, themeService); 23 | const appMenu = Menu.buildFromTemplate(appMenuTemplate); 24 | Menu.setApplicationMenu(appMenu); 25 | } 26 | 27 | //////////////////////////////////////////////////////////////////////////////// 28 | 29 | function createNoteworthy(): NoteworthyApp { 30 | /** FSAL: File System Abstraction Layer */ 31 | const fsal = new FSALSystem(); 32 | fsal.init(); 33 | 34 | const workspaceService = new WorkspaceService(fsal); 35 | const pluginService = new PluginService(workspaceService); 36 | const themeService = new ThemeService(fsal); 37 | 38 | const app = new NoteworthyApp( 39 | fsal, 40 | workspaceService, 41 | pluginService, 42 | themeService 43 | ); 44 | 45 | createAppMenu(app, themeService); 46 | 47 | return app; 48 | } 49 | 50 | //////////////////////////////////////////////////////////////////////////////// 51 | 52 | // This method will be called when Electron has finished 53 | // initialization and is ready to create browser windows. 54 | // Some APIs can only be used after this event occurs. 55 | app.whenReady().then(() => { 56 | // Set app user model id for windows 57 | electronApp.setAppUserModelId('com.electron') 58 | 59 | // Default open or close DevTools by F12 in development 60 | // and ignore CommandOrControl + R in production. 61 | // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils 62 | app.on('browser-window-created', (_, window) => { 63 | optimizer.watchWindowShortcuts(window) 64 | }) 65 | 66 | createNoteworthy(); 67 | 68 | // app.on('activate', function () { 69 | // // On macOS it's common to re-create a window in the app when the 70 | // // dock icon is clicked and there are no other windows open. 71 | // if (BrowserWindow.getAllWindows().length === 0) createWindow() 72 | // }) 73 | }); 74 | 75 | // Quit when all windows are closed, except on macOS. There, it's common 76 | // for applications and their menu bar to stay active until the user quits 77 | // explicitly with Cmd + Q. 78 | app.on('window-all-closed', () => { 79 | if (process.platform !== 'darwin') { 80 | app.quit(); 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/ipc/citation.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Cite } from "@citation-js/core"; 3 | import "@citation-js/plugin-bibtex"; 4 | import "@citation-js/plugin-csl"; 5 | import { MainIpcChannel } from "@main/MainIPC"; 6 | import NoteworthyApp from "@main/app"; 7 | import { PluginService } from "@main/plugins/plugin-service"; 8 | 9 | declare global { 10 | namespace Noteworthy { 11 | export interface MainIpcHandlers { 12 | citations: MainIpc_CitationHandlers; 13 | } 14 | } 15 | } 16 | 17 | export class MainIpc_CitationHandlers implements MainIpcChannel { 18 | get name() { return "citations" as const; } 19 | 20 | constructor( 21 | private _app: NoteworthyApp, 22 | private _pluginService: PluginService 23 | ) {} 24 | 25 | /** 26 | * Convert a citation key (such as `peyton-jones1992:stg`) into 27 | * a formatted citation suitable for display. 28 | */ 29 | async getCitationForKey(citeKey: string): Promise { 30 | const citePlugin = this._pluginService.getWorkspacePluginByName("citation_plugin"); 31 | if(!citePlugin) { return null; } 32 | 33 | // get file corresponding to citation key, if one exists 34 | // TODO (2022/03/07) what if two files exist for the same tag? which citation to use? 35 | const file = await this._app._eventHandlers.tag.getFileForTag({ tag: citeKey, create: false }); 36 | if(!file) { return null; } 37 | 38 | const cite = this.getCitationForHash(file.hash); 39 | return cite; 40 | 41 | // TODO (2022/03/07) also check if any bibliography files contain the key 42 | // TODO (2022/03/07) what if bibliography contains entry whose key matches a file tag? 43 | } 44 | 45 | getCitationForHash(hash: string): string | null { 46 | const citePlugin = this._pluginService.getWorkspacePluginByName("citation_plugin"); 47 | if(!citePlugin) { return null; } 48 | 49 | // retrieve citation string from document 50 | const citeData = citePlugin.getCitationForHash(hash); 51 | if(!citeData) { return null; } 52 | 53 | // use citeproc-js to render citation string 54 | try { 55 | const cite = new Cite(citeData.data); 56 | 57 | const citeOutput = cite.format('bibliography', { 58 | type: 'html', 59 | template: 'vancouver', 60 | lang: 'en-US' 61 | }); 62 | 63 | if(typeof citeOutput !== "string") { return null; } 64 | return citeOutput; 65 | } catch(err) { 66 | console.error(err); 67 | return null; 68 | } 69 | } 70 | 71 | async generateBibliography(citeKeys: string[]): Promise { 72 | const citePlugin = this._pluginService.getWorkspacePluginByName("citation_plugin"); 73 | if(!citePlugin) { return null; } 74 | 75 | // retrieve bibliography entry for each citation key 76 | const citeData: (string|object[]|object)[] = []; 77 | for(const citeKey of citeKeys) { 78 | // get file corresponding to citation key, if one exists 79 | // TODO (2022/03/07) what if two files exist for the same tag? which citation to use? 80 | const file = await this._app._eventHandlers.tag.getFileForTag({ tag: citeKey, create: false }); 81 | if(!file) { continue; } 82 | 83 | // get citation data 84 | const data = citePlugin.getCitationForHash(file.hash); 85 | if(!data) { continue; } 86 | 87 | citeData.push(data.data); 88 | } 89 | 90 | // generate bibliography 91 | const cite = new Cite(citeData); 92 | const bibliography = cite.format("bibliography", { 93 | format: "html", 94 | template: "apa", 95 | lang: "en-US" 96 | }); 97 | 98 | return bibliography as string; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/ipc/dialog.ts: -------------------------------------------------------------------------------- 1 | import { DialogSaveDiscardOptions } from "@common/dialog"; 2 | import { IPossiblyUntitledFile } from "@common/files"; 3 | import { MainIpcChannel } from "@main/MainIPC"; 4 | import NoteworthyApp from "@main/app"; 5 | import { FSAL } from "@main/fsal/fsal"; 6 | import { WorkspaceService } from "@main/workspace/workspace-service"; 7 | import { dialog } from "electron"; 8 | 9 | export class MainIpc_DialogHandlers implements MainIpcChannel { 10 | 11 | get name() { return "dialog" as const; } 12 | 13 | /** @todo (9/13/20) break app into multiple parts so we don't need to consume the whole thing */ 14 | constructor( 15 | private _app: NoteworthyApp, 16 | private _fsal: FSAL, 17 | private _workspaceService: WorkspaceService 18 | ){ } 19 | 20 | // -- Show Notification ----------------------------- // 21 | 22 | showNotification(msg: string) { 23 | /** @todo (6/26/20) implement notifications */ 24 | } 25 | 26 | showError(msg: string) { 27 | /** @todo (6/26/20) implement error notifications */ 28 | } 29 | 30 | // -- Request File Open ----------------------------- // 31 | 32 | dialogFileOpen() { 33 | if (!this._app.window) { return; } 34 | 35 | // open file dialog 36 | const filePaths: string[] | undefined = dialog.showOpenDialogSync( 37 | this._app.window.window, 38 | { 39 | properties: ['openFile'], 40 | //filters: FILE_FILTERS 41 | } 42 | ); 43 | // if no path selected, do nothing 44 | if (!filePaths || !filePaths.length) return; 45 | 46 | throw new Error("MainIpc_DialogHandlers :: opening individual file from path is not implemented"); 47 | 48 | // load file from path 49 | //this._navigationHandlers.requestFileOpen({ path: filePaths[0] }) 50 | } 51 | 52 | // -- Dialog File Create ---------------------------- // 53 | 54 | async dialogFileNewPath(): Promise { 55 | if (!this._app.window) { return Promise.reject("no active window"); } 56 | 57 | // default "new file" path 58 | const workspaceDir = this._workspaceService.getWorkspaceDir(); 59 | 60 | const newFilePath: string | undefined = dialog.showSaveDialogSync( 61 | this._app.window.window, 62 | { 63 | title: "New Document", 64 | buttonLabel: "New Document", 65 | properties: ["showOverwriteConfirmation"], 66 | ...(workspaceDir && { defaultPath: workspaceDir.path }) 67 | } 68 | ); 69 | if (!newFilePath) { return Promise.reject("no file path specified"); } 70 | else { return newFilePath; } 71 | } 72 | 73 | async dialogFileNew(): Promise { 74 | const newFilePath = await this.dialogFileNewPath(); 75 | 76 | // create and open new file 77 | let newFile = await this._workspaceService.createFile(newFilePath, ""); 78 | if(!newFile) { return Promise.reject("failed to create new file"); } 79 | 80 | return this._app._eventHandlers.navigation.navigateToHash({ hash: newFile.hash }); 81 | } 82 | 83 | // -- Dialog File Save As --------------------------- // 84 | 85 | async dialogFileSaveAs(file: IPossiblyUntitledFile): Promise { 86 | if (!this._app.window) { return null; } 87 | 88 | const newFilePath: string | undefined = dialog.showSaveDialogSync( 89 | //TODO: better default "save as" path? 90 | this._app.window.window, 91 | { 92 | defaultPath: file.path || "", 93 | //filters: FILE_FILTERS 94 | } 95 | ); 96 | if (!newFilePath) return null; 97 | this._fsal.saveFile(newFilePath, file.contents, false); 98 | 99 | // send new file path to renderer 100 | this._app._renderProxy?.fileDidSave({ saveas: true, path: newFilePath}); 101 | return newFilePath; 102 | } 103 | 104 | // -- Ask Save/Discard Changes ---------------------- // 105 | 106 | /** @todo (7/12/20) better return type? extract array type? **/ 107 | async askSaveDiscardChanges(filePath: string): Promise { 108 | if (!this._app.window) { throw new Error("no window open! cannot open dialog!"); } 109 | let response = await dialog.showMessageBox(this._app.window.window, { 110 | type: "warning", 111 | title: "Warning: Unsaved Changes", 112 | message: `File (${filePath}) contains unsaved changes.`, 113 | buttons: Array.from(DialogSaveDiscardOptions), 114 | defaultId: DialogSaveDiscardOptions.indexOf("Save"), 115 | cancelId: DialogSaveDiscardOptions.indexOf("Cancel"), 116 | }) 117 | return DialogSaveDiscardOptions[response.response]; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/ipc/file.ts: -------------------------------------------------------------------------------- 1 | import { IFileMeta, IFileWithContents } from "@common/files"; 2 | import { MainIpcChannel } from "@main/MainIPC"; 3 | import NoteworthyApp from "@main/app"; 4 | import { FSAL } from "@main/fsal/fsal"; 5 | import { WorkspaceService } from "@main/workspace/workspace-service"; 6 | 7 | //////////////////////////////////////////////////////////// 8 | 9 | export class MainIpc_FileHandlers implements MainIpcChannel { 10 | get name() { return "file" as const; } 11 | 12 | constructor( 13 | private _app: NoteworthyApp, 14 | private _fsal: FSAL, 15 | private _workspaceService: WorkspaceService 16 | ){ } 17 | 18 | // -- Request File Create --------------------------- // 19 | 20 | async requestFileCreate(path:string, contents:string=""):Promise { 21 | /** @todo (6/26/20) check if path in workspace? */ 22 | return this._workspaceService.createFile(path, contents); 23 | } 24 | 25 | // -- Request File Save ----------------------------- // 26 | 27 | async requestFileSave(file: IFileWithContents): Promise { 28 | if (!this._app.window) { return false; } 29 | 30 | await this._fsal.saveFile(file.path, file.contents, false); 31 | /** @todo (7/12/20) check for file save errors? */ 32 | this._app._renderProxy?.fileDidSave({saveas: false, path: file.path }); 33 | return true; 34 | } 35 | 36 | // -- Request File Open ----------------------------- // 37 | 38 | async requestFileContents(fileInfo: { hash?: string }):Promise { 39 | let { hash } = fileInfo; 40 | // validate input 41 | if (hash === undefined) { 42 | console.error("MainIPC :: requestFileContents() :: no hash provided"); 43 | return null; 44 | } 45 | 46 | // load from hash 47 | let fileMeta: IFileMeta | null; 48 | if (hash === undefined || !(fileMeta = this._workspaceService.getFileByHash(hash))) { 49 | /** @todo (6/20/20) load from arbitrary path */ 50 | console.log(hash, hash && this._workspaceService.getFileByHash(hash)); 51 | console.error("file loading from arbitrary path not implemented"); 52 | return null; 53 | } 54 | 55 | // read file contents 56 | const fileContents: string | null = this._fsal.readFile(fileMeta.path); 57 | if (fileContents === null) { throw new Error("MainIPC :: failed to read file"); } 58 | 59 | let file: IFileWithContents = { 60 | parent: null, 61 | contents: fileContents, 62 | ...fileMeta 63 | } 64 | 65 | return file; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/ipc/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { to } from "@common/util/to"; 2 | import { MainIpcChannel } from "@main/MainIPC"; 3 | import NoteworthyApp from "@main/app"; 4 | 5 | //////////////////////////////////////////////////////////// 6 | 7 | export class MainIpc_LifecycleHandlers implements MainIpcChannel { 8 | 9 | get name() { return "lifecycle" as const; } 10 | 11 | /** @todo (9/13/20) break app into multiple parts so we don't need to consume the whole thing */ 12 | constructor(private _app:NoteworthyApp){ } 13 | 14 | // -- Quit ------------------------------------------ // 15 | 16 | async requestAppQuit():Promise{ 17 | /** @todo (7/12/20) handle multiple windows? multiple files open? */ 18 | if(this._app._renderProxy){ 19 | // attempt to close active editors/windows 20 | let [err, result] = await to(this._app._renderProxy.requestClose()); 21 | // ok if promise rejects because user cancelled shutdown 22 | if(err == "Cancel"){ return; } 23 | // anything else is an error 24 | else if(err){ return Promise.reject(err); } 25 | } 26 | // close app 27 | this._app.quit(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/ipc/metadata.ts: -------------------------------------------------------------------------------- 1 | import { MainIpcChannel } from "@main/MainIPC"; 2 | import { IMetadata } from "@main/plugins/metadata-plugin"; 3 | import { PluginService } from "@main/plugins/plugin-service"; 4 | 5 | //////////////////////////////////////////////////////////// 6 | 7 | export class MainIpc_MetadataHandlers implements MainIpcChannel { 8 | 9 | get name() { return "metadata" as const; } 10 | 11 | constructor( 12 | private _pluginService: PluginService 13 | ) { } 14 | 15 | async getMetadataForHash(hash: string): Promise { 16 | let plugin = this._pluginService.getWorkspacePluginByName("metadata_plugin"); 17 | if(!plugin){ console.error("no plugin!"); return null; } 18 | console.log(`getMetadataForHash :: ${hash}`); 19 | return plugin.getMetadataForHash(hash); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/ipc/navigation.ts: -------------------------------------------------------------------------------- 1 | import { IFileMeta, getFileMetadata } from "@common/files"; 2 | import { MainIpcChannel } from "@main/MainIPC"; 3 | import NoteworthyApp from "@main/app"; 4 | import { PluginService } from "@main/plugins/plugin-service"; 5 | import { WorkspaceService } from "@main/workspace/workspace-service"; 6 | 7 | //////////////////////////////////////////////////////////// 8 | 9 | export class MainIpc_NavigationHandlers implements MainIpcChannel { 10 | 11 | get name() { return "navigation" as const; } 12 | 13 | // TODO (2021/03/12) clear navigation history on workspace open/close 14 | private _navHistory: IFileMeta[]; 15 | private _navIdx: number; 16 | 17 | constructor( 18 | private _app:NoteworthyApp, 19 | private _workspaceService: WorkspaceService, 20 | private _pluginService: PluginService 21 | ) { 22 | this._navHistory = []; 23 | this._navIdx = 0; 24 | } 25 | 26 | /** 27 | * @returns Metadata for the opened file, if successful, otherwise null 28 | */ 29 | private async _navigate(fileInfo: { hash: string }): Promise { 30 | console.log(`MainIPC_NavigationHandlers :: navigate :: ${ fileInfo.hash }`); 31 | 32 | // get file contents 33 | // @todo (2022/03/04) avoid private access to _eventHandlers? or make public? 34 | let file = await this._app._eventHandlers.file.requestFileContents(fileInfo); 35 | if(!file){ return null; } 36 | 37 | // send file to render process 38 | this._app._renderProxy?.fileDidOpen(file); 39 | 40 | // return 41 | return getFileMetadata(file); 42 | } 43 | 44 | public getNavigationHistory() { 45 | return { 46 | history: [...this._navHistory], 47 | currentIdx: this._navIdx 48 | } 49 | } 50 | 51 | public navigateNext(): void { 52 | // clamp to guarantee valid output index, even if we receive invalid input 53 | let nextIdx: number = Math.min(Math.max(this._navIdx, 0), this._navHistory.length); 54 | 55 | // search forwards through history for next valid index 56 | let foundValid: boolean = false; 57 | while(nextIdx + 1 < this._navHistory.length){ 58 | nextIdx = nextIdx + 1; 59 | if(this._workspaceService.getFileByHash(this._navHistory[nextIdx].hash)) { 60 | foundValid = true; 61 | break; 62 | } 63 | } 64 | 65 | // do nothing if no valid files found 66 | if(!foundValid || nextIdx === this._navIdx) { return; } 67 | 68 | // navigate 69 | let file = this._navigate({ hash: this._navHistory[nextIdx].hash }); 70 | if(!file) { return; } 71 | this._navIdx = nextIdx; 72 | 73 | // TODO (2021/03/12) re-think updates to reactive ui data 74 | this._app._renderProxy?.navHistoryChanged({ history: this._navHistory, currentIdx: this._navIdx }); 75 | } 76 | 77 | public navigatePrev(): void { 78 | // clamp to guarantee valid output index, even if we receive invalid input 79 | let prevIdx: number = Math.min(Math.max(this._navIdx, 0), this._navHistory.length); 80 | 81 | // search forwards through history for next valid index 82 | let foundValid: boolean = false; 83 | while(prevIdx - 1 > 0){ 84 | prevIdx = prevIdx - 1; 85 | if(this._workspaceService.getFileByHash(this._navHistory[prevIdx].hash)) { 86 | foundValid = true; 87 | break; 88 | } 89 | } 90 | 91 | // do nothing if no valid files found 92 | if(!foundValid || prevIdx === this._navIdx) { return; } 93 | 94 | // navigate 95 | let file = this._navigate({ hash: this._navHistory[prevIdx].hash }); 96 | if(!file) { return; } 97 | this._navIdx = prevIdx; 98 | 99 | // TODO (2021/03/12) re-think updates to reactive ui data 100 | this._app._renderProxy?.navHistoryChanged({ history: this._navHistory, currentIdx: this._navIdx }); 101 | } 102 | 103 | async navigateToHash(fileInfo: { hash: string }): Promise { 104 | if (!this._app.window) { return; } 105 | 106 | // request file contents 107 | let file = await this._navigate(fileInfo); 108 | if(!file){ return; } 109 | 110 | // push this file onto navigation stack, erasing any existing forward history 111 | this._navHistory.splice(this._navIdx+1, this._navHistory.length-this._navIdx+1, file); 112 | this._navIdx = this._navHistory.length - 1; 113 | 114 | // TODO (2021/03/12) re-think updates to reactive ui data 115 | this._app._renderProxy?.navHistoryChanged({ history: this._navHistory, currentIdx: this._navIdx }); 116 | } 117 | 118 | async navigateToTag(data:{tag: string, create:boolean, directoryHint?:string}):Promise { 119 | if (!this._app.window) { return; } 120 | 121 | // get files which define this tag 122 | // @todo (2022/03/04) avoid private access to _eventHandlers? or make public? 123 | let fileHash = await this._app._eventHandlers.tag.getHashForTag(data); 124 | if(!fileHash) return; 125 | // load file from hash 126 | this.navigateToHash({ hash: fileHash }); 127 | } 128 | 129 | async navigateToIndex(idx: number): Promise { 130 | if(idx < 0 || idx >= this._navHistory.length) { 131 | return Promise.reject("MainIpc_NavigationHandlers :: navigateToIndex :: index out of bounds"); 132 | } 133 | 134 | this._navigate({ hash: this._navHistory[idx].hash }); 135 | this._navIdx = idx; 136 | 137 | // TODO (2021/03/12) re-think updates to reactive ui data 138 | this._app._renderProxy?.navHistoryChanged({ history: this._navHistory, currentIdx: this._navIdx }); 139 | 140 | // required by tsconfig `noImplicitReturns` 141 | return; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/ipc/outline.ts: -------------------------------------------------------------------------------- 1 | import { MainIpcChannel } from "@main/MainIPC"; 2 | import { IOutline } from "@main/plugins/outline-plugin"; 3 | import { PluginService } from "@main/plugins/plugin-service"; 4 | 5 | export class MainIpc_OutlineHandlers implements MainIpcChannel { 6 | 7 | get name() { return "outline" as const; } 8 | 9 | constructor( 10 | private _pluginService:PluginService 11 | ) { } 12 | 13 | async requestOutlineForHash(hash: string): Promise { 14 | // get active plugin 15 | let plugin = this._pluginService.getWorkspacePluginByName("outline_plugin"); 16 | if(!plugin){ return []; } 17 | // get outline 18 | return plugin.getOutlineForHash(hash); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/ipc/shell.ts: -------------------------------------------------------------------------------- 1 | import { MainIpcChannel } from "@main/MainIPC"; 2 | import { shell } from "electron"; 3 | 4 | //////////////////////////////////////////////////////////// 5 | 6 | export class MainIpc_ShellHandlers implements MainIpcChannel { 7 | 8 | get name() { return "shell" as const; } 9 | 10 | constructor() { } 11 | 12 | async requestExternalLinkOpen(url: string) { 13 | shell.openExternal(url, { activate: true }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/ipc/theme.ts: -------------------------------------------------------------------------------- 1 | import { MainIpcChannel } from "@main/MainIPC"; 2 | import { ThemeService } from "@main/theme/theme-service"; 3 | 4 | export class MainIpc_ThemeHandlers implements MainIpcChannel { 5 | 6 | get name() { return "theme" as const; } 7 | 8 | /** @todo (9/13/20) break app into multiple parts so we don't need to consume the whole thing */ 9 | constructor(private _themeService:ThemeService){ } 10 | 11 | async requestThemeRefresh() { 12 | this._themeService.setTheme(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/ipc/workspace.ts: -------------------------------------------------------------------------------- 1 | import { MainIpcChannel } from "@main/MainIPC"; 2 | import NoteworthyApp from "@main/app"; 3 | import { WorkspaceService } from "@main/workspace/workspace-service"; 4 | import { dialog } from "electron"; 5 | import { MainIpc_DialogHandlers } from "./dialog"; 6 | import { IDirectory } from "@common/files"; 7 | 8 | export class MainIPC_WorkspaceHandlers implements MainIpcChannel { 9 | 10 | get name() { return "workspace" as const; } 11 | 12 | constructor( 13 | private _app: NoteworthyApp, 14 | private _workspaceService: WorkspaceService, 15 | private _dialogHandlers: MainIpc_DialogHandlers, 16 | ) { } 17 | 18 | async selectWorkspace() { 19 | if (!this._app.window) { return; } 20 | 21 | // open file dialog 22 | const dirPaths: string[] | undefined = dialog.showOpenDialogSync( 23 | this._app.window.window, 24 | { 25 | properties: ['openDirectory', 'createDirectory'], 26 | //filters: FILE_FILTERS 27 | } 28 | ); 29 | if (!dirPaths || !dirPaths.length) return; 30 | 31 | this._workspaceService.setWorkspaceDir(dirPaths[0]); 32 | } 33 | 34 | async newFilePrompt() { 35 | const newFilePath = await this._dialogHandlers.dialogFileNewPath(); 36 | 37 | // create and open new file 38 | let newFile = await this._workspaceService.createFile(newFilePath, ""); 39 | if(!newFile) { return Promise.reject("failed to create new file"); } 40 | 41 | return this._app._eventHandlers.navigation.navigateToHash({ hash: newFile.hash }); 42 | } 43 | 44 | async currentWorkspaceDir(): Promise { 45 | let dir = this._workspaceService.getWorkspaceDir(); 46 | if(dir) { return dir.path; } 47 | else { return null; } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/menus/app-menu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides functions to create the global application menu. 3 | * 4 | * Currently, Electron does not support dynamic menus, so 5 | * it is necessary to re-create the entire menu whenever a 6 | * menu item needs updating. 7 | * 8 | * https://github.com/electron/electron/issues/2717 9 | */ 10 | 11 | 12 | import { MenuItemConstructorOptions } from "electron"; 13 | import { is } from "electron-util"; 14 | import NoteworthyApp from "@main/app"; 15 | import { shell } from "electron"; 16 | import { ThemeService } from "@main/theme/theme-service"; 17 | 18 | //////////////////////////////////////////////////////////// 19 | 20 | function makeFileMenu(app:NoteworthyApp): MenuItemConstructorOptions { 21 | /** @todo (9/13/20) don't keep `app` arg in closures!! */ 22 | return { 23 | label: "File", 24 | submenu: [ 25 | { 26 | label: "Open Workspace...", 27 | click: () => { app.handle("workspace", "selectWorkspace"); } 28 | }, 29 | { 30 | label: "Open File...", 31 | click: () => { app.handle("dialog", "dialogFileOpen"); } 32 | }, 33 | { 34 | type: "separator" 35 | }, 36 | { 37 | label: "Save", 38 | click: () => { 39 | if(!app._renderProxy){ console.error("no proxy!"); return; } 40 | app._renderProxy.menuFileSave() 41 | } 42 | }, 43 | { 44 | label: "Save As...", 45 | click: () => { 46 | if(!app._renderProxy){ console.error("no proxy!"); return; } 47 | app._renderProxy.menuFileSaveAs() 48 | } 49 | }, 50 | { 51 | type: "separator" 52 | }, 53 | { 54 | label: "Exit", 55 | click: () => { } 56 | } 57 | ] 58 | } 59 | } 60 | 61 | function makeViewMenu(): MenuItemConstructorOptions { 62 | return { 63 | label: "View", 64 | submenu: [ 65 | { role: "reload" }, 66 | { role: "forceReload" }, 67 | { role: "toggleDevTools" }, 68 | { type: "separator" }, 69 | { role: "resetZoom" }, 70 | { role: "zoomIn" }, 71 | { role: "zoomOut" }, 72 | { type: "separator" }, 73 | { type: "separator" }, 74 | { role: "togglefullscreen" } 75 | ] 76 | }; 77 | } 78 | 79 | async function makeThemeMenu(themeService:ThemeService): Promise { 80 | // fetch themes 81 | let themes = await themeService.getThemes(); 82 | 83 | let defaultThemeSubmenu: MenuItemConstructorOptions[] = themes.default.map(theme => ({ 84 | label: theme.title, 85 | click: () => { themeService.setTheme({ type: "default", id: theme.id }); } 86 | })); 87 | 88 | let customThemeSubmenu: MenuItemConstructorOptions[] = themes.custom.map(theme => ({ 89 | label: theme.title, 90 | click: () => { themeService.setTheme({ type: "custom", path: theme.path }); } 91 | })); 92 | 93 | let submenu: MenuItemConstructorOptions[] = [ 94 | { 95 | label: "Open Themes Folder...", 96 | click: () => { shell.openPath(themeService.getThemeFolder()); } 97 | }, 98 | { 99 | label: "Refresh Custom Themes", 100 | click: () => { themeService.refreshCustomThemes(); } }, 101 | { type: "separator" } 102 | ]; 103 | 104 | submenu = submenu.concat( 105 | defaultThemeSubmenu, 106 | [ { type: "separator" } ], 107 | customThemeSubmenu, 108 | ); 109 | 110 | return { 111 | label: "Theme", 112 | submenu 113 | }; 114 | } 115 | 116 | function makeWindowMenu(app:NoteworthyApp): MenuItemConstructorOptions { 117 | return { 118 | label: "Window", 119 | submenu: [ 120 | { role: "minimize" }, 121 | { 122 | label: "Close", 123 | click: () => { app.handle("lifecycle", "requestAppQuit") } 124 | }, 125 | { role: "zoom", visible: is.macos } 126 | ] 127 | }; 128 | } 129 | 130 | export async function makeAppMenuTemplate(app:NoteworthyApp, themeService:ThemeService): Promise { 131 | return [ 132 | makeFileMenu(app), 133 | makeViewMenu(), 134 | await makeThemeMenu(themeService), 135 | makeWindowMenu(app), 136 | ]; 137 | } 138 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/plugins/citation-plugin.ts: -------------------------------------------------------------------------------- 1 | // project imports 2 | import { IWorkspaceDir, IFileMeta } from "@common/files"; 3 | import { WorkspacePlugin } from "./plugin"; 4 | import { IDoc } from "@common/doctypes/doctypes"; 5 | 6 | //////////////////////////////////////////////////////////// 7 | 8 | export type Citation 9 | = { type: "bibtex", data: string } 10 | | { 11 | type: "bibtex-json", 12 | data: { 13 | type: string, 14 | label: string|null, 15 | properties: { [key: string]: string } 16 | } 17 | }; 18 | 19 | /** 20 | * Document types should implement this interface in order 21 | * to be recognized by the built-in cross-reference system. 22 | */ 23 | export interface ICitationProvider { 24 | /** @todo (7/28/20) better solution? */ 25 | IS_CITATION_PROVIDER:true; 26 | getCitation(): Citation | null; 27 | } 28 | 29 | export function isCitationProvider(resource:unknown):resource is ICitationProvider { 30 | return (resource as ICitationProvider).IS_CITATION_PROVIDER === true; 31 | } 32 | 33 | //////////////////////////////////////////////////////////// 34 | 35 | declare global { 36 | namespace Noteworthy { 37 | export interface Plugins { 38 | citation_plugin: CitationPlugin; 39 | } 40 | } 41 | } 42 | 43 | export class CitationPlugin implements WorkspacePlugin { 44 | 45 | get plugin_name() :string { return "citation_plugin"; } 46 | 47 | // plugin data 48 | _doc2cite: { [hash:string] : Citation }; 49 | 50 | constructor(){ 51 | console.log(`${this.plugin_name} :: constructor()`); 52 | this._doc2cite = { }; 53 | } 54 | 55 | // == Lifecycle ===================================== // 56 | 57 | async init():Promise { 58 | console.log(`${this.plugin_name} :: init()`); 59 | this.attachEvents(); 60 | } 61 | 62 | attachEvents(){} 63 | detachEvents(){} 64 | 65 | dispose():void { 66 | this.clear(); 67 | this.detachEvents(); 68 | } 69 | 70 | clear(): void { 71 | this._doc2cite = { }; 72 | } 73 | 74 | // == Workspace Events ============================== // 75 | 76 | async handleWorkspaceClosed(dir: IWorkspaceDir){ 77 | console.log(`${this.plugin_name} :: handle(workspace-closed)`); 78 | this.clear(); 79 | } 80 | 81 | async handleWorkspaceOpen(dir: IWorkspaceDir) { 82 | console.log(`${this.plugin_name} :: handle(workspace-open)`); 83 | /** @todo (6/18/20) */ 84 | } 85 | 86 | handleFileDeleted(filePath: string, fileHash: string): void { 87 | // remove metadata information for this doc 88 | delete this._doc2cite[fileHash]; 89 | } 90 | 91 | handleFileCreated(fileMeta: IFileMeta, doc: IDoc): void { 92 | if(!isCitationProvider(doc)) { return; } 93 | this._addCitationFor(fileMeta, doc); 94 | } 95 | 96 | handleFileChanged(fileMeta: IFileMeta, doc: IDoc): void { 97 | if(!isCitationProvider(doc)) { return; } 98 | this._addCitationFor(fileMeta, doc); 99 | } 100 | 101 | // == Outline Management ============================ // 102 | 103 | private _addCitationFor(fileMeta: IFileMeta, doc: ICitationProvider): void { 104 | // do nothing if file does not provide a citation 105 | const cite = doc.getCitation(); 106 | if(cite === null) { return; } 107 | 108 | // if doc does not specify a citation label, use the filename 109 | if(cite.type === "bibtex-json" && !cite.data.label) { 110 | cite.data.label = fileMeta.name; 111 | } 112 | 113 | console.log("_addMetadataFor", fileMeta.hash, cite); 114 | this._doc2cite[fileMeta.hash] = cite; 115 | } 116 | 117 | getCitationForHash(hash: string): Citation | null { 118 | const result = this._doc2cite[hash]; 119 | console.log("CitationPlugin :: ", hash, result); 120 | return result || null; 121 | } 122 | 123 | // == Persistence =================================== // 124 | 125 | serialize():string { 126 | return JSON.stringify({ 127 | doc2cite: this._doc2cite 128 | }) 129 | } 130 | 131 | deserialize(serialized:string): CitationPlugin { 132 | let json: any = JSON.parse(serialized); 133 | this._doc2cite = json.doc2cite; 134 | /** @todo: validate that deserialized data is actually valid */ 135 | return this; 136 | } 137 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/main/plugins/metadata-plugin.ts: -------------------------------------------------------------------------------- 1 | // project imports 2 | import { IWorkspaceDir, IFileMeta } from "@common/files"; 3 | import { WorkspacePlugin } from "./plugin"; 4 | import { IDoc } from "@common/doctypes/doctypes"; 5 | 6 | //////////////////////////////////////////////////////////// 7 | 8 | /** 9 | * @todo (7/28/20) properly validate YAML metadata 10 | */ 11 | export interface MetadataFields { 12 | title?:string; 13 | author?:string; 14 | authors?:string; // TODO (2022/03/06) author vs authors? 15 | url?:string; 16 | date?:string; 17 | year?:string; 18 | tags_defined?:string; // TODO (2022/03/06) yaml might give us a string[] instead 19 | tags?:string; // TODO (2022/03/06) yaml might give us a string[] instead 20 | bibtex?:string; 21 | } 22 | 23 | /** 24 | * Type representing file metadata. 25 | */ 26 | export type IMetadata = MetadataFields & { [key:string] : string|string[] }; 27 | 28 | /** 29 | * Document types should implement this interface in order 30 | * to be recognized by the built-in cross-reference system. 31 | */ 32 | export interface IMetadataProvider { 33 | /** @todo (7/28/20) better solution? */ 34 | IS_METADATA_PROVIDER:true; 35 | getMetadata():IMetadata; 36 | } 37 | 38 | export function isMetadataProvider(resource:unknown):resource is IMetadataProvider { 39 | return (resource as IMetadataProvider).IS_METADATA_PROVIDER === true; 40 | } 41 | 42 | //////////////////////////////////////////////////////////// 43 | 44 | export class MetadataPlugin implements WorkspacePlugin { 45 | 46 | plugin_name:string = "metadata_plugin"; 47 | 48 | // plugin data 49 | _doc2meta: { [hash:string] : IMetadata }; 50 | 51 | constructor(){ 52 | console.log(`metadata-plugin :: constructor()`); 53 | this._doc2meta = { }; 54 | } 55 | 56 | // == Lifecycle ===================================== // 57 | 58 | async init():Promise { 59 | console.log("metadata-plugin :: init()"); 60 | this.attachEvents(); 61 | } 62 | 63 | attachEvents(){} 64 | detachEvents(){} 65 | 66 | dispose():void { 67 | this.clear(); 68 | this.detachEvents(); 69 | } 70 | 71 | clear(): void { 72 | this._doc2meta = { }; 73 | } 74 | 75 | // == Workspace Events ============================== // 76 | 77 | async handleWorkspaceClosed(dir: IWorkspaceDir){ 78 | console.log(`${this.plugin_name} :: handle(workspace-closed)`); 79 | this.clear(); 80 | } 81 | 82 | async handleWorkspaceOpen(dir: IWorkspaceDir) { 83 | console.log(`${this.plugin_name} :: handle(workspace-open)`); 84 | /** @todo (6/18/20) */ 85 | } 86 | 87 | handleFileDeleted(filePath:string, fileHash:string): void { 88 | // remove metadata information for this doc 89 | delete this._doc2meta[fileHash]; 90 | } 91 | 92 | handleFileCreated(fileMeta:IFileMeta, doc:IDoc): void { 93 | if(!isMetadataProvider(doc)) { return; } 94 | this._addMetadataFor(fileMeta, doc); 95 | } 96 | 97 | handleFileChanged(fileMeta:IFileMeta, doc:IDoc): void { 98 | if(!isMetadataProvider(doc)) { return; } 99 | this._addMetadataFor(fileMeta, doc); 100 | } 101 | 102 | // == Outline Management ============================ // 103 | 104 | private _addMetadataFor(fileMeta:IFileMeta, doc:IMetadataProvider): void { 105 | this._doc2meta[fileMeta.hash] = doc.getMetadata(); 106 | } 107 | 108 | getMetadataForHash(hash:string): IMetadata | null { 109 | return this._doc2meta[hash] || null; 110 | } 111 | 112 | // == Persistence =================================== // 113 | 114 | serialize():string { 115 | return JSON.stringify({ 116 | doc2meta: this._doc2meta 117 | }) 118 | } 119 | 120 | deserialize(serialized:string): MetadataPlugin { 121 | let json: any = JSON.parse(serialized); 122 | this._doc2meta = json.doc2meta; 123 | /** @todo: validate that deserialized data is actually valid */ 124 | return this; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/plugins/outline-plugin.ts: -------------------------------------------------------------------------------- 1 | // project imports 2 | import { IWorkspaceDir, IFileMeta } from "@common/files"; 3 | import { WorkspacePlugin } from "./plugin"; 4 | import { IDoc } from "@common/doctypes/doctypes"; 5 | 6 | //////////////////////////////////////////////////////////// 7 | 8 | /** 9 | * Document types should implement this interface in order 10 | * to be recognized by the built-in cross-reference system. 11 | */ 12 | export interface IOutlineProvider { 13 | /** @todo (7/28/20) better solution? */ 14 | IS_OUTLINE_PROVIDER:true; 15 | 16 | /** 17 | * Compute a flattened outline for this resource. 18 | * @returns An ordered list of outline entries (such as 19 | * headings or other major semantic elements) 20 | */ 21 | getOutline():IOutlineEntry[]; 22 | } 23 | 24 | export function isOutlineProvider(resource:unknown):resource is IOutlineProvider { 25 | return (resource as IOutlineProvider).IS_OUTLINE_PROVIDER === true; 26 | } 27 | 28 | export type IOutlineEntry = { 29 | /** Depths define an implicit tree structure among entries, beginning with 1 at the top level. */ 30 | depth : number, 31 | /** A string identifier for this entry, unique within the document. */ 32 | uniqueId: string, 33 | /** A (potentially non-unique) label for this entry. */ 34 | label : string 35 | }; 36 | 37 | /** 38 | * An outline is an ordered collection of IOutlineEntry objects, 39 | * whose `.depth` property defines an implicit tree structure. 40 | */ 41 | export type IOutline = IOutlineEntry[]; 42 | 43 | //////////////////////////////////////////////////////////// 44 | 45 | export class OutlinePlugin implements WorkspacePlugin { 46 | 47 | plugin_name:string = "outline_plugin"; 48 | 49 | // plugin data 50 | _doc2outline: { [hash:string] : IOutline }; 51 | 52 | constructor(){ 53 | console.log(`outline-plugin :: constructor()`); 54 | this._doc2outline = { }; 55 | } 56 | 57 | // == Lifecycle ===================================== // 58 | 59 | async init():Promise { 60 | console.log("outline-plugin :: init()"); 61 | this.attachEvents(); 62 | } 63 | 64 | attachEvents(){} 65 | detachEvents(){} 66 | 67 | dispose():void { 68 | this.clear(); 69 | this.detachEvents(); 70 | } 71 | 72 | clear(): void { 73 | this._doc2outline = { }; 74 | } 75 | 76 | // == Workspace Events ============================== // 77 | 78 | async handleWorkspaceClosed(dir: IWorkspaceDir){ 79 | console.log("xref-plugin :: handle(workspace-closed)"); 80 | this.clear(); 81 | } 82 | 83 | async handleWorkspaceOpen(dir: IWorkspaceDir) { 84 | console.log("xref-plugin :: handle(workspace-open)"); 85 | /** @todo (6/18/20) */ 86 | } 87 | 88 | handleFileDeleted(filePath:string, fileHash:string): void { 89 | // remove outline information for this doc 90 | delete this._doc2outline[fileHash]; 91 | } 92 | 93 | handleFileCreated(fileMeta:IFileMeta, doc:IDoc): void { 94 | if(!isOutlineProvider(doc)) { return; } 95 | 96 | // create outline 97 | this._addOutlineFor(fileMeta, doc); 98 | } 99 | 100 | handleFileChanged(fileMeta:IFileMeta, doc:IDoc): void { 101 | if(!isOutlineProvider(doc)) { return; } 102 | 103 | // re-compute outline 104 | this._addOutlineFor(fileMeta, doc); 105 | } 106 | 107 | // == Outline Management ============================ // 108 | 109 | private _addOutlineFor(fileMeta:IFileMeta, doc:IOutlineProvider): void { 110 | // compute outline 111 | let outline:IOutlineEntry[] = doc.getOutline(); 112 | this._doc2outline[fileMeta.hash] = outline; 113 | } 114 | 115 | getOutlineForHash(hash:string): IOutline | null { 116 | return this._doc2outline[hash] || null; 117 | } 118 | 119 | // == Persistence =================================== // 120 | 121 | serialize():string { 122 | return JSON.stringify({ 123 | doc2outline: this._doc2outline 124 | }) 125 | } 126 | 127 | deserialize(serialized:string): OutlinePlugin { 128 | let json: any = JSON.parse(serialized); 129 | this._doc2outline = json.doc2outline; 130 | /** @todo: validate that deserialized data is actually valid */ 131 | return this; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/plugins/plugin-service.ts: -------------------------------------------------------------------------------- 1 | /** @todo (9/13/20) This should probably be merged into either 2 | * the WorkspaceService or the CrossRefPlugin, but it was put 3 | * here to avoid major changes to how tags work while 4 | * refactoring IPC code. 5 | */ 6 | 7 | import { WorkspaceService } from "@main/workspace/workspace-service"; 8 | import { WorkspacePlugin } from "./plugin"; 9 | import { OutlinePlugin } from "./outline-plugin"; 10 | import { CrossRefPlugin } from "./crossref-plugin"; 11 | import { MetadataPlugin } from "./metadata-plugin"; 12 | 13 | //////////////////////////////////////////////////////////////////////////////// 14 | 15 | export declare const BasePlugins: { 16 | readonly crossref_plugin: CrossRefPlugin; 17 | readonly outline_plugin: OutlinePlugin; 18 | readonly metadata_plugin: MetadataPlugin; 19 | }; 20 | 21 | declare global { 22 | namespace Noteworthy { 23 | /** This interface is for extending the default 24 | * set of Plugins with full type-checking support. */ 25 | export interface Plugins { 26 | 27 | } 28 | } 29 | } 30 | 31 | export type Plugins = Noteworthy.Plugins & typeof BasePlugins; 32 | export type PluginName = keyof Plugins; 33 | export type PluginType = Plugins[keyof Plugins]; 34 | 35 | //////////////////////////////////////////////////////////////////////////////// 36 | 37 | export class PluginService { 38 | constructor( 39 | private _workspaceService:WorkspaceService 40 | ){} 41 | 42 | /** 43 | * Retrieve the workspace plugin with the specified name. 44 | * @returns NULL when the plugin is not available 45 | */ 46 | getWorkspacePluginByName(name: T): Plugins[T] | null { 47 | if(!this._workspaceService.workspace) { return null; } 48 | const plugin = this._workspaceService.workspace.getPluginByName(name); 49 | // @todo (2022/03/04) avoid cast here? better guarantee? 50 | // @todo (2022/03/04) avoid returning null? 51 | return plugin as Plugins[T] || null; 52 | } 53 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/main/plugins/plugin.ts: -------------------------------------------------------------------------------- 1 | import { IWorkspaceDir, IFileMeta } from "@common/files"; 2 | import { IDoc } from "@common/doctypes/doctypes"; 3 | import { IDisposable } from "@common/types"; 4 | 5 | //////////////////////////////////////////////////////////// 6 | 7 | export interface WorkspacePlugin extends IDisposable { 8 | plugin_name:string; 9 | 10 | handleWorkspaceClosed(dir:IWorkspaceDir):void; 11 | handleWorkspaceOpen(dir:IWorkspaceDir):void; 12 | 13 | /** @todo (6/19/20) hash can be computed from path, so don't pass it in? */ 14 | handleFileDeleted(filePath:string, fileHash:string):void; 15 | handleFileCreated(fileMeta:IFileMeta, contents:IDoc):void; 16 | handleFileChanged(fileMeta:IFileMeta, contents:IDoc):void; 17 | 18 | serialize():string; 19 | deserialize(serialized:string):WorkspacePlugin; 20 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/main/theme/theme-service.ts: -------------------------------------------------------------------------------- 1 | // project imports 2 | import { Settings } from "@common/settings"; 3 | 4 | // electron imports 5 | import { app } from "electron"; 6 | 7 | // node.js imports 8 | import * as pathlib from "path"; 9 | import { promises as fs } from "fs"; 10 | import { EventEmitter } from "events"; 11 | import { FSAL } from "@main/fsal/fsal"; 12 | import { FsalEvents, ChokidarEvents } from "@common/events"; 13 | 14 | // @ts-ignore (vite asset) 15 | import themeDefaultLight from "@resources/themes/theme-default-light.css?raw"; 16 | // @ts-ignore (vite asset) 17 | import themeDefaultDark from "@resources/themes/theme-default-dark.css?raw"; 18 | // @ts-ignore (vite asset) 19 | import themeAcademicLight from "@resources/themes/theme-academic-light.css?raw"; 20 | // @ts-ignore (vite asset) 21 | import themeTypewriterLight from "@resources/themes/theme-typewriter-light.css?raw"; 22 | 23 | 24 | export enum ThemeEvent { 25 | THEME_CHANGED = "theme-changed" 26 | } 27 | 28 | /** @todo (9/14/20) respond to darkMode.onChange events from eletron-util */ 29 | 30 | //////////////////////////////////////////////////////////// 31 | 32 | export type ThemeId = { type: "default", id:string } | { type: "custom", path:string }; 33 | 34 | export class ThemeService extends EventEmitter { 35 | constructor(private _fsal: FSAL){ 36 | super(); 37 | this.initThemeFolder(); 38 | } 39 | 40 | // == Lifecycle ===================================== // 41 | 42 | async initThemeFolder(){ 43 | // ensure theme folder exists 44 | let themeFolder = this.getThemeFolder(); 45 | fs.mkdir(themeFolder) 46 | .then(()=> { console.log("app :: theme folder created at", themeFolder); }) 47 | .catch(()=> { console.log("app :: theme folder already exists at", themeFolder); }); 48 | 49 | // watch for changes 50 | this._fsal.watchGlobal(themeFolder); 51 | this._fsal.addListener(FsalEvents.GLOBAL_CHOKIDAR_EVENT, (event:ChokidarEvents, info:{ path:string })=>{ 52 | console.log("theme-service :: chokidar event"); 53 | if(info.path.startsWith(themeFolder)){ 54 | console.log("theme-service :: Theme Folder Changed!"); 55 | this.refreshCustomThemes(); 56 | } 57 | }); 58 | } 59 | 60 | // == Theme Configuration =========================== // 61 | 62 | async refreshCustomThemes(){ 63 | /** @todo (9/14/20) refresh theme folder list and emit event which triggers re-creation of application menu */ 64 | this.refreshTheme(); 65 | } 66 | 67 | async refreshTheme(){ 68 | this.setTheme(null); 69 | } 70 | 71 | async setTheme(theme:ThemeId|null = null) { 72 | // use current theme if none provided 73 | if(theme == null){ theme = Settings.get("theme"); } 74 | 75 | // default vs custom themes 76 | if(theme.type == "default"){ 77 | // find default theme 78 | let cssString:string = ""; 79 | switch(theme.id){ 80 | /** @todo (9/14/20) these should be defined elsewhere */ 81 | case "default-dark" : cssString = themeDefaultDark; break; 82 | case "default-light" : cssString = themeDefaultLight; break; 83 | case "typewriter-light" : cssString = themeTypewriterLight; break; 84 | case "academic-light" : cssString = themeAcademicLight; break; 85 | default: console.error(`theme '${theme.id}' not found`); return; 86 | } 87 | 88 | this.emit(ThemeEvent.THEME_CHANGED, cssString); 89 | 90 | // save theme to user settings 91 | Settings.set("theme", theme); 92 | } else if(theme.type == "custom"){ 93 | // read and apply theme 94 | let cssString:string = await fs.readFile(theme.path, { encoding : 'utf8' }); 95 | this.emit(ThemeEvent.THEME_CHANGED, cssString); 96 | // save theme to user settings 97 | Settings.set("theme", theme); 98 | } 99 | } 100 | 101 | getThemeFolder(): string { 102 | /** @todo (9/12/20) 103 | * userData folder is different in develop vs production, 104 | * still need to test that this works in production 105 | */ 106 | return pathlib.join(app.getPath("userData"), "themes"); 107 | } 108 | 109 | async getThemes() { 110 | return { 111 | "default" : [ 112 | { title: "Default Light", id: "default-light" }, 113 | { title: "Default Dark", id: "default-dark" }, 114 | { title: "Typewriter Light", id: "typewriter-light" }, 115 | { title: "Academic Light", id: "academic-light" }, 116 | ], 117 | "custom" : await this.getCustomThemes() 118 | }; 119 | } 120 | 121 | async getCustomThemes(): Promise<{ title:string, path:string }[]> { 122 | // attempt to read themes folder, but fail gracefully when it does not exist 123 | let themeFolder = this.getThemeFolder(); 124 | 125 | let filePaths:string[] = []; 126 | try { 127 | filePaths = await fs.readdir(themeFolder); 128 | } catch(err){ 129 | console.error("themes folder does not exist\n", err); 130 | } 131 | 132 | // filter .css files 133 | return filePaths 134 | .filter(fileName => (pathlib.extname(fileName)==".css")) 135 | .map(fileName => { 136 | let path = pathlib.join(themeFolder, fileName); 137 | return ({ title: fileName, path: path }) 138 | }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /noteworthy-electron/src/main/windows/MainWindow.ts: -------------------------------------------------------------------------------- 1 | // electron 2 | import { shell, BrowserWindow, Event, ipcMain, IpcMainEvent } from 'electron'; 3 | import windowStateKeeper from 'electron-window-state'; 4 | import { is } from "@electron-toolkit/utils"; 5 | 6 | // node 7 | import path from "path"; 8 | 9 | // noteworthy 10 | import NoteworthyApp from '@main/app'; 11 | import { randomId } from "@common/util/random"; 12 | 13 | // assets 14 | // @ts-ignore (vite asset) 15 | import icon from '../../../resources/icon.png?asset' 16 | 17 | //////////////////////////////////////////////////////////// 18 | 19 | export default class MainWindow { 20 | 21 | window: BrowserWindow; 22 | 23 | /* ==== CONSTRUCTOR =================================== */ 24 | 25 | constructor(private _app:NoteworthyApp){ 26 | 27 | // ---- window ----------------------------------------- 28 | 29 | const mainWindow = new BrowserWindow({ 30 | frame: true, 31 | width: 900, 32 | height: 760, 33 | show: false, 34 | icon: icon, 35 | title: "Noteworthy", 36 | webPreferences: { 37 | webSecurity: true, 38 | sandbox: true, 39 | contextIsolation: true, 40 | preload: path.join(__dirname, '../preload/preload.js'), 41 | }, 42 | // icon: "assets/icon/noteworthy-icon-512.png" 43 | }); 44 | 45 | this.window = mainWindow; 46 | 47 | mainWindow.on('ready-to-show', () => { 48 | mainWindow.show() 49 | }) 50 | 51 | mainWindow.webContents.setWindowOpenHandler((details) => { 52 | // TODO (Ben @ 2023/05/04) this was copied from vite-electron 53 | shell.openExternal(details.url); 54 | return { action: 'deny' }; 55 | }); 56 | 57 | // hot module reloading from electron-vite 58 | if (is.dev && process.env['ELECTRON_RENDERER_URL']) { 59 | mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) 60 | } else { 61 | mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')) 62 | } 63 | 64 | // ---- events ----------------------------------------- 65 | 66 | this.window.on("close", this.handleClose); 67 | } 68 | 69 | /* ==== IPC =========================================== */ 70 | 71 | async invoke(channel:string, ...args:any[]):Promise { 72 | return new Promise((resolve, reject) => { 73 | // generate unique id for event 74 | let responseId = `RENDER_DID_HANDLE::${channel}::${randomId()}`; 75 | // send message from main --> render 76 | this.window.webContents.send(channel, responseId, ...args); 77 | // expect response -- promise won't resolve otherwise 78 | /** @todo (7/12/20) accept timeout (seconds) as argument? */ 79 | ipcMain.once(responseId, (evt: IpcMainEvent, success:boolean, result:any) => { 80 | if(success) { resolve(result); } 81 | else { reject(result); } 82 | }); 83 | }); 84 | } 85 | 86 | /* ==== EVENTS ======================================== */ 87 | 88 | handleClose = (event: Event) => { 89 | console.log("main :: handleClose"); 90 | // when this is true, the app has decided it's OK to quit 91 | if (global.isQuitting) { return; } 92 | // otherwise, we need to decide whether it's OK to quit 93 | event.preventDefault(); 94 | this._app.handle("lifecycle", "requestAppQuit"); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /noteworthy-electron/src/patch.d.ts: -------------------------------------------------------------------------------- 1 | // type declarations 2 | 3 | interface ProxyConstructor { 4 | new (target: TSource, handler: ProxyHandler): TTarget; 5 | } 6 | 7 | // -- Remark / Micromark / Mdast ------------------------ // 8 | 9 | declare module "remark-wiki-link" { 10 | import { Plugin } from "unified" 11 | 12 | let wikiLinkPlugin : Plugin; 13 | } 14 | 15 | declare module "micromark/dist/util/chunked-splice" { 16 | function chunkedSplice(list: T[], start: number, remove: number, items: T[]): void; 17 | export = chunkedSplice; 18 | } 19 | 20 | declare module "micromark/dist/util/shallow" { 21 | function shallow(object: T): T; 22 | export = shallow; 23 | } 24 | 25 | declare module "micromark/dist/util/resolve-all" { 26 | import { Construct, Event, Tokenizer } from "micromark/dist/shared-types"; 27 | function resolveAll(constructs: Construct[], events: Event[], context: Tokenizer): any; 28 | export = resolveAll; 29 | } 30 | 31 | declare module "mdast-util-to-markdown/lib/util/safe.js" { 32 | import { Context, SafeOptions } from "mdast-util-to-markdown"; 33 | 34 | // as of (2021-05-07) this function had no exported typings 35 | function safe(context: Context, input: string, config: Partial ): string; 36 | export = safe 37 | } 38 | 39 | declare module "micromark-extension-wiki-link" { 40 | function syntax(opts?: { aliasDivider?: string }): any; 41 | } 42 | 43 | // -- Required by @common/remark-plugins/concrete/remark-concrete -------------- 44 | 45 | declare module "repeat-string" { 46 | /** 47 | * Repeat the given string the specified number of times. 48 | */ 49 | function repeat(str: string, count: number): string; 50 | export = repeat; 51 | } 52 | 53 | declare module "mdast-util-to-markdown/lib/util/check-rule-repeat" { 54 | import { Context } from "mdast-util-to-markdown"; 55 | 56 | /** 57 | * Returns the number of repetitions to use when serializing thematic breaks. 58 | * https://github.com/syntax-tree/mdast-util-to-markdown/blob/f3df7410049ed426ef8734ec762a38aa2feee73f/lib/util/check-rule-repeat.js#L3 59 | */ 60 | function checkRepeat(context: Context): number; 61 | export = checkRepeat; 62 | } 63 | 64 | declare module "mdast-util-to-markdown/lib/util/check-rule" { 65 | import { Context } from "mdast-util-to-markdown"; 66 | 67 | /** 68 | * Returns the marker that should be used to serialize thematic breaks. 69 | * https://github.com/syntax-tree/mdast-util-to-markdown/blob/f3df7410049ed426ef8734ec762a38aa2feee73f/lib/util/check-rule.js#L3 70 | */ 71 | function checkRepeat(context: Context): string; 72 | export = checkRepeat; 73 | } 74 | 75 | declare module "mdast-util-to-markdown/lib/util/check-bullet" { 76 | import { Context } from "mdast-util-to-markdown"; 77 | /** 78 | * Returns the default bullet style for list items. 79 | * https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/util/check-bullet.js#L3 80 | */ 81 | function checkBullet(context: Context): "*"|"-"|"+"; 82 | export = checkBullet; 83 | } 84 | 85 | declare module "mdast-util-to-markdown/lib/util/check-list-item-indent" { 86 | import { Context } from "mdast-util-to-markdown"; 87 | /** 88 | * Returns the default indent style for list items. 89 | * https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/util/check-list-item-indent.js#L3 90 | */ 91 | function checkListItemIndent(context: Context): 'one'|'tab'|'mixed'; 92 | export = checkListItemIndent; 93 | } 94 | 95 | declare module "mdast-util-to-markdown/lib/util/container-flow" { 96 | import { Context } from "mdast-util-to-markdown"; 97 | import { Parent } from "unist"; 98 | 99 | /** 100 | * https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/util/container-flow.js#L5 101 | */ 102 | function flow(parent: Parent, context: Context): string ; 103 | export = flow; 104 | } 105 | 106 | declare module "mdast-util-to-markdown/lib/util/indent-lines" { 107 | /** 108 | * https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/util/indent-lines.js#L5 109 | */ 110 | function indentLines(value: string, map: (line: string, index: number, blank: boolean) => string): string; 111 | export = indentLines; 112 | } 113 | -------------------------------------------------------------------------------- /noteworthy-electron/src/preload/preload.d.ts: -------------------------------------------------------------------------------- 1 | import { ClipboardApi } from "./preload"; 2 | 3 | declare global { 4 | interface Window { 5 | clipboardApi: ClipboardApi 6 | restrictedIpcRenderer: RestrictedIpcRenderer 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /noteworthy-electron/src/preload/preload.ts: -------------------------------------------------------------------------------- 1 | import { IpcEvents } from "@common/events"; 2 | import { 3 | contextBridge, 4 | ipcRenderer, 5 | clipboard 6 | } from "electron"; 7 | 8 | //////////////////////////////////////////////////////////////////////////////// 9 | 10 | // TODO: (2021/03/05) This `restrictedIpcRenderer` code was added to allow ipc 11 | // communication when `contextIsolation = true`, as the Electron developers have 12 | // (thankfully) begun to enforce more strict security measures by default. 13 | // 14 | // This bit of code should probably be merged with the IPC proxy objects in 15 | // ipc.ts, since they accomplish very similar things. For the time being, they 16 | // remain separate to minimze the number of changes needed to update to the 17 | // newest version of Electron (12.0.0). 18 | 19 | export interface RestrictedIpcRenderer { 20 | send: (channel:string, ...data:any) => void, 21 | receive: (channel:string, listener: (...args: any[]) => void) => void, 22 | invoke: (channel:string, listener: (...args: any[]) => void) => void 23 | } 24 | 25 | // https://github.com/electron/electron/issues/9920#issuecomment-575839738 26 | // Expose protected methods that allow the renderer process to use 27 | // the ipcRenderer without exposing the entire object 28 | let restrictedIpcRenderer: RestrictedIpcRenderer = { 29 | send: async (channel:string, ...data:any[]) => { 30 | // whitelist channels 31 | let validChannels: string[] = ["command"]; 32 | if (validChannels.includes(channel) || channel.startsWith("RENDER_DID_HANDLE")) { 33 | console.log(`preload :: send() :: channel=${channel} :: ${data}`); 34 | return await ipcRenderer.send(channel, ...data); 35 | } else { 36 | console.log(`preload :: send() :: invalid channel '${channel}'!`); 37 | } 38 | }, 39 | receive: (channel:string, listener: (...args: any[]) => void) => { 40 | //let validChannels: string[] = [IpcEvents.RENDERER_INVOKE]; 41 | //TODO: (2021/03/05) send message over sub-channel instead of using prefix 42 | if (channel.startsWith("RENDER_DID_HANDLE") || channel.startsWith(IpcEvents.RENDERER_INVOKE)) { 43 | console.log(`preload :: attaching listener for channel=${channel}`); 44 | // deliberately strip event as it includes `sender` 45 | ipcRenderer.on(channel, (event, ...args) => { 46 | console.log(`preload :: received message :: channel=${channel}`); 47 | listener(...args); 48 | }); 49 | } else { 50 | console.log(`preload :: receive() :: invalid channel '${channel}'`); 51 | } 52 | }, 53 | invoke: async (channel:string, ...data:any[]) => { 54 | let validChannels: string[] = ["command"]; 55 | if (validChannels.includes(channel)) { 56 | console.log(`preload :: invoke() :: channel=${channel} :: ${data}`); 57 | return await ipcRenderer.invoke(channel, ...data) 58 | } else { 59 | console.log(`preload :: invoke() :: invalid channel '${channel}' :: ${data}`); 60 | } 61 | } 62 | } 63 | 64 | contextBridge.exposeInMainWorld( 65 | "restrictedIpcRenderer", restrictedIpcRenderer 66 | ); 67 | 68 | //////////////////////////////////////////////////////////////////////////////// 69 | 70 | export type ClipboardApi = { 71 | getClipboardImageDataURI(): string|null; 72 | } 73 | 74 | let clipboardApi: ClipboardApi = { 75 | /** 76 | * Convert incoming clipboard images to Base64-encoded data URIs. 77 | */ 78 | getClipboardImageDataURI(): string|null { 79 | // despite what the docs say, Electron's clipboard is occasionally `undefined` 80 | if(!clipboard) { 81 | console.error("[preload.clipboardApi] clipboard is undefined:", clipboard); 82 | return null; 83 | } 84 | 85 | var formats = clipboard.availableFormats("clipboard"); 86 | var hasImage = formats.find(str => str.startsWith("image")); 87 | console.warn("[clipboardApi] available formats:", formats); 88 | 89 | // check for images on the clipboard 90 | if(hasImage){ 91 | var image = clipboard.readImage("clipboard"); 92 | var dataUrl = image.toDataURL(); 93 | console.warn("[clipboardApi] image=", image); 94 | console.warn("[clipboardApi] data=", dataUrl); 95 | 96 | return dataUrl; 97 | } 98 | // ignore all other data types 99 | return null; 100 | } 101 | } 102 | 103 | contextBridge.exposeInMainWorld( 104 | "clipboardApi", clipboardApi 105 | ); 106 | -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Noteworthy 6 | 7 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/RendererIPC.ts: -------------------------------------------------------------------------------- 1 | import Renderer, { IRendererState } from "./render"; 2 | import { IPossiblyUntitledFile, IDirEntryMeta } from "@common/files"; 3 | 4 | //////////////////////////////////////////////////////////// 5 | 6 | export class RendererIpcHandlers { 7 | private _renderer:Renderer; 8 | 9 | constructor(renderer:Renderer){ 10 | this._renderer = renderer; 11 | } 12 | 13 | async menuFileSave():Promise { 14 | return this._renderer._editor?.saveCurrentFile(false); 15 | } 16 | 17 | async menuFileSaveAs():Promise { 18 | return this._renderer._editor?.saveCurrentFile(true); 19 | } 20 | 21 | async fileTreeChanged(fileTree:IDirEntryMeta[]):Promise{ 22 | console.log("RenderIPC :: fileTreeChanged", fileTree.map(val=>val.name)); 23 | this._renderer.setFileTree(fileTree); 24 | } 25 | 26 | async fileDidSave(data:{ saveas:boolean , path:string }):Promise { 27 | return this._renderer._editor?.handleFileDidSave() 28 | } 29 | 30 | async fileDidOpen(file:IPossiblyUntitledFile):Promise { 31 | return this._renderer.setCurrentFile(file); 32 | } 33 | 34 | async navHistoryChanged(history: IRendererState["navigationHistory"]){ 35 | // TODO (2021/03/12) navigation should probably be handled entirely in the render process 36 | this._renderer.setNavHistory(history); 37 | } 38 | 39 | /** 40 | * @returns FALSE if file failed to close due to unsaved changes, 41 | * TRUE otherwise. Useful for deciding whether app can quit. 42 | */ 43 | async requestFileClose():Promise { 44 | if(!this._renderer._editor){ console.error("no editor to close!"); return; } 45 | let result = await this._renderer._editor.closeAndDestroy(); 46 | return; 47 | } 48 | 49 | /** 50 | * @rejects when user cancels the close operation due to unsaved changes 51 | */ 52 | async requestClose(): Promise { 53 | /** @todo (7/12/20) close multiple open files? */ 54 | return this.requestFileClose(); 55 | } 56 | 57 | async applyThemeCss(cssString:string){ 58 | this._renderer.applyThemeCss(cssString); 59 | } 60 | } 61 | 62 | export type RendererIpcEvents = keyof RendererIpcHandlers; -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-Bold.woff2 -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-BoldItalic.woff2 -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-Italic.woff2 -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-Medium.woff2 -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-MediumItalic.woff2 -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-Regular.woff2 -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-SemiBold.woff2 -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-SemiBoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/lora/Lora-SemiBoldItalic.woff2 -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/lora/lora.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Lora'; 3 | font-style: italic; 4 | font-weight: 400; 5 | font-display: swap; 6 | src: url(Lora-Italic.woff2) format('woff2'); 7 | } 8 | 9 | @font-face { 10 | font-family: 'Lora'; 11 | font-style: italic; 12 | font-weight: 600; 13 | font-display: swap; 14 | src: url(Lora-SemiBoldItalic.woff2) format('woff2'); 15 | } 16 | 17 | @font-face { 18 | font-family: 'Lora'; 19 | font-style: normal; 20 | font-weight: 400; 21 | font-display: swap; 22 | src: url(Lora-Regular.woff2) format('woff2'); 23 | } 24 | 25 | @font-face { 26 | font-family: 'Lora'; 27 | font-style: normal; 28 | font-weight: 600; 29 | font-display: swap; 30 | src: url(Lora-SemiBold.woff2) format('woff2'); 31 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/assets/fonts/roboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/assets/fonts/roboto/roboto.css: -------------------------------------------------------------------------------- 1 | /* light italic */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: italic; 5 | font-weight: 300; 6 | font-display: swap; 7 | src: url('Roboto-LightItalic.ttf'); 8 | } 9 | /* italic */ 10 | @font-face { 11 | font-family: 'Roboto'; 12 | font-style: italic; 13 | font-weight: 400; 14 | font-display: swap; 15 | src: url('Roboto-Italic.ttf'); 16 | } 17 | /* medium italic */ 18 | @font-face { 19 | font-family: 'Roboto'; 20 | font-style: italic; 21 | font-weight: 500; 22 | font-display: swap; 23 | src: url('Roboto-MediumItalic.ttf'); 24 | } 25 | /* bold italic */ 26 | @font-face { 27 | font-family: 'Roboto'; 28 | font-style: italic; 29 | font-weight: 700; 30 | font-display: swap; 31 | src: url('Roboto-BoldItalic.ttf'); 32 | } 33 | /* light */ 34 | @font-face { 35 | font-family: 'Roboto'; 36 | font-style: normal; 37 | font-weight: 300; 38 | font-display: swap; 39 | src: url('Roboto-Light.ttf'); 40 | } 41 | /* regular */ 42 | @font-face { 43 | font-family: 'Roboto'; 44 | font-style: normal; 45 | font-weight: 400; 46 | font-display: swap; 47 | src: url('Roboto-Regular.ttf'); 48 | } 49 | /* medium */ 50 | @font-face { 51 | font-family: 'Roboto'; 52 | font-style: normal; 53 | font-weight: 500; 54 | font-display: swap; 55 | src: url('Roboto-Medium.ttf'); 56 | } 57 | /* bold */ 58 | @font-face { 59 | font-family: 'Roboto'; 60 | font-style: normal; 61 | font-weight: 700; 62 | font-display: swap; 63 | src: url('Roboto-Bold.ttf'); 64 | } 65 | -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/codicon/codicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benrbray/noteworthy/5c82c646b5d9784338bb542530906889dbda702c/noteworthy-electron/src/renderer/src/codicon/codicon.ttf -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/commandManager.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler, CommandArg, RegisteredCommandName, CommandResult } from "@common/commands/commands"; 2 | 3 | export interface CommandEvents { 4 | commandsChanged: { } 5 | } 6 | 7 | export class CommandManager { 8 | 9 | private _registeredCommands 10 | = new Map< 11 | RegisteredCommandName, 12 | (arg: any, resolveCommand: (result: any) => void, rejectCommand: () => void) => Promise 13 | >(); 14 | 15 | // internal services can subscribe to events which report changes to the list of commands 16 | private _eventHandlers: { [E in keyof CommandEvents] : ((arg: CommandEvents[E]) => void)[] } 17 | = { commandsChanged: [] }; 18 | 19 | constructor ( 20 | ) { 21 | 22 | } 23 | 24 | getCommandNames(): RegisteredCommandName[] { 25 | return Array.from(this._registeredCommands.keys()); 26 | } 27 | 28 | registerCommand( 29 | name: C, 30 | command: CommandHandler 31 | ): void { 32 | if(this._registeredCommands.has(name)) { 33 | console.error(`command ${name} already registered`); 34 | } 35 | 36 | this._registeredCommands.set(name, command); 37 | 38 | // emit events 39 | this._emitEvent("commandsChanged", {}); 40 | } 41 | 42 | async executeCommand( 43 | name: C, 44 | arg: CommandArg 45 | ): Promise> { 46 | const command = this._registeredCommands.get(name); 47 | 48 | if(!command) { 49 | console.error(`unknown command ${name}`); 50 | return Promise.reject(); 51 | } 52 | 53 | return new Promise((resolveCommand, rejectCommand) => { 54 | return command(arg, resolveCommand, rejectCommand); 55 | }); 56 | } 57 | 58 | /* ---- event emitter --------------------------------- */ 59 | 60 | private async _emitEvent( 61 | event: E, 62 | arg: CommandEvents[E] 63 | ): Promise { 64 | this._eventHandlers[event].forEach((handler) => { 65 | handler(arg); 66 | }); 67 | } 68 | 69 | on(event: E, handler: (arg: CommandEvents[E]) => void) { 70 | this._eventHandlers[event].push(handler); 71 | } 72 | 73 | 74 | } 75 | -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/commands/newFileCommand.tsx: -------------------------------------------------------------------------------- 1 | import { NoteworthyExtensionApi } from "@common/extensions/extension-api"; 2 | import { MainIpcHandlers } from "@main/MainIPC"; 3 | import { ModalActions } from "@renderer/ui/Modal/modal"; 4 | import { ModalNewFile, ModalNewFileProps } from "@renderer/ui/ModalNewFile/ModalNewFile"; 5 | import { render } from "solid-js/web"; 6 | 7 | declare module "@common/commands/commands" { 8 | export interface InternalCommands { 9 | newFile: CommandSpec< 10 | {}, 11 | string|null 12 | > 13 | } 14 | } 15 | 16 | export const initNewFileCommand = ( 17 | api: NoteworthyExtensionApi, 18 | mainProxy: MainIpcHandlers 19 | ) => { 20 | api.registerCommand("newFile", async (arg, resolveCommand, rejectCommand) => { 21 | console.log("[newFile]"); 22 | 23 | const workspaceDir = await mainProxy.workspace.currentWorkspaceDir(); 24 | if(!workspaceDir) { 25 | console.error("cannot create new file when no workspace is open"); 26 | return null; 27 | } 28 | 29 | const props = (modalActions: ModalActions): ModalNewFileProps => ({ 30 | promptFilePath : async () => { 31 | return mainProxy.dialog.dialogFileNewPath(); 32 | }, 33 | handleSubmit(name: string) { 34 | modalActions.close(); 35 | resolveCommand(name); 36 | }, 37 | handleCancel() { 38 | modalActions.close(); 39 | }, 40 | workspaceRoot : workspaceDir 41 | }); 42 | 43 | await api.executeCommand("showModal", { 44 | title: "New File", 45 | renderModal: (dom, modalActions) => { 46 | render(() => , dom); 47 | } 48 | }); 49 | 50 | // if we reach this point without resolving 51 | // the promise, no file has been selected 52 | return null; 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/index.tsx: -------------------------------------------------------------------------------- 1 | import Renderer from './render'; 2 | 3 | // library css 4 | import "@benrbray/prosemirror-math/dist/prosemirror-math.css"; 5 | import "prosemirror-view/style/prosemirror.css"; 6 | import "katex/dist/katex.min.css"; 7 | import "prosemirror-gapcursor/style/gapcursor.css"; 8 | import "./codicon/codicon.css"; 9 | 10 | // project css 11 | import "./assets/main.css"; 12 | import "./assets/editor.css"; 13 | 14 | //////////////////////////////////////////////////////////// 15 | 16 | window.onload = function() { 17 | let renderer: Renderer = new Renderer(); 18 | renderer.init(); 19 | } 20 | -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/ui/Modal/modal.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: fixed; 3 | left: 0; 4 | top: 0; 5 | bottom: 0; 6 | right: 0; 7 | 8 | background-color: #0008; 9 | backdrop-filter: hue-rotate(120deg); 10 | } 11 | 12 | .modal-box { 13 | /* centering */ 14 | position: absolute; 15 | left: 50%; 16 | top: 50%; 17 | transform: translate(-50%, -50%); 18 | 19 | width: min(80%, 40rem); 20 | 21 | /* layout */ 22 | display: flex; 23 | flex-direction: column; 24 | 25 | /* style */ 26 | box-sizing: border-box; 27 | padding: 1rem; 28 | 29 | border-radius: 0.5rem; 30 | background-color: white; 31 | 32 | box-shadow: inset; 33 | box-shadow: 34 | rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, 35 | rgba(15, 15, 15, 0.1) 0px 4px 8px; 36 | } 37 | 38 | .modal-title { 39 | font-weight: bold; 40 | font-size: 1.2rem; 41 | margin-bottom: 1.0rem; 42 | } 43 | 44 | .modal-content { 45 | flex: 1; 46 | margin-bottom: 1rem; 47 | } 48 | 49 | .modal-button-row { 50 | display: flex; 51 | flex-direction: row; 52 | justify-content: flex-end; 53 | gap: 0.5rem; 54 | } 55 | 56 | .modal-button { 57 | font-size: 1rem; 58 | } 59 | -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/ui/Modal/modal.tsx: -------------------------------------------------------------------------------- 1 | import { NoteworthyExtensionApi } from "@common/extensions/extension-api"; 2 | import { Ref, Show, createEffect } from "solid-js"; 3 | 4 | import "./modal.css" 5 | import { CommandSpec } from "@common/commands/commands"; 6 | 7 | export interface ModalActions { 8 | close: () => Promise; 9 | } 10 | 11 | /* ---- modal component --------------------------------- */ 12 | 13 | type RenderModalFn = (dom : HTMLElement, actions: ModalActions) => void; 14 | 15 | export interface ModalProps { 16 | setModalState: (setter: (state: ModalState) => ModalState) => void, 17 | data: null | { 18 | renderModal: RenderModalFn, 19 | title: string, 20 | }, 21 | } 22 | 23 | export const Modal = (props: ModalProps) => { 24 | let contentRef: Ref; 25 | const actions: ModalActions = { 26 | async close() { 27 | props.setModalState(state => { 28 | return ModalController.hideModal(state); 29 | }); 30 | } 31 | } 32 | 33 | const handleClickBackground = (evt: MouseEvent) => { 34 | if(evt.target !== evt.currentTarget) return; 35 | actions.close(); 36 | } 37 | 38 | return ( 39 | 37 | )} 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/ui/historyTab.tsx: -------------------------------------------------------------------------------- 1 | import { IFileMeta } from "@common/files"; 2 | import { For, Suspense } from "solid-js"; 3 | 4 | //////////////////////////////////////////////////////////// 5 | 6 | interface IHistoryTabProps { 7 | //outline: IOutline | null; 8 | navHistory: { history: IFileMeta[], currentIdx: number }; 9 | handleHistoryClick: (evt:MouseEvent) => void; 10 | } 11 | 12 | export const HistoryTab = (props: IHistoryTabProps) => { 13 | // render 14 | return (
15 | 16 | no outline!
}> 17 | {(entry: IFileMeta, idx)=>( 18 |
24 | {entry.name} 25 |
26 | )} 27 | 28 | 29 | ) 30 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/ui/loading.tsx: -------------------------------------------------------------------------------- 1 | export const Loading = () => { 2 | return (
loading...
); 3 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/ui/outlineTab.tsx: -------------------------------------------------------------------------------- 1 | import { For, createResource, Suspense } from "solid-js"; 2 | import { IOutline, IOutlineEntry } from "@main/plugins/outline-plugin"; 3 | 4 | //////////////////////////////////////////////////////////// 5 | 6 | interface IOutlineTabProps { 7 | //outline: IOutline | null; 8 | getOutline: () => Promise; 9 | } 10 | 11 | export const OutlineTab = (props:IOutlineTabProps) => { 12 | // fetch outline 13 | const [res] = createResource<{ outline: IOutline|null }>( 14 | async (k, getPrev) => ({ 15 | outline: await props.getOutline() 16 | }), 17 | ); 18 | 19 | // render 20 | return (
21 | 22 | no outline!
}> 23 | {(entry:IOutlineEntry)=>( 24 |
25 | {entry.label} 26 |
27 | )} 28 | 29 | 30 | ) 31 | } -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/ui/panelBacklinks.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, For, createResource, JSX } from "solid-js"; 2 | import { MainIpcHandlers } from "@main/MainIPC"; 3 | import { IFileMeta } from "@common/files"; 4 | 5 | interface PanelBacklinksProps { 6 | proxy: MainIpcHandlers; 7 | hash: string|null; 8 | fileName: string|null; 9 | handleFileClick: (event:MouseEvent)=>void; 10 | } 11 | 12 | interface BacklinksData { 13 | data: { citation: string | null, meta: IFileMeta }[]; 14 | } 15 | 16 | export const PanelBacklinks = (props: PanelBacklinksProps) => { 17 | 18 | const [backlinks] = createResource( 19 | () => props, 20 | async (pr, getPrev) => { 21 | if(!pr.hash) { return { data: [] }; } 22 | 23 | const search = await pr.proxy.tag.backlinkSearch(pr.hash); 24 | const result = await Promise.all(search.map(async (meta) => { 25 | const citation = await pr.proxy.citations.getCitationForHash(meta.hash); 26 | return { citation, meta }; 27 | } 28 | )) 29 | 30 | console.log("backlinks\n", JSON.stringify(result, undefined, 2)); 31 | 32 | return { data: result } 33 | } 34 | ) 35 | 36 | return (
37 |
{`backlinks for ${props.fileName || ""}`}
38 |
39 | loading...
}> 40 |
    41 | 42 | {backlink => ( 43 |
  • 44 | {backlink.meta.name} 45 | {backlink.citation || "<>"} 46 | {backlink.meta.path} 47 |
  • 48 | )} 49 |
    50 |
51 | 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/ui/tag-search.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, For, createResource, JSX } from "solid-js"; 2 | import { Loading } from "./loading"; 3 | import { ITagSearchResult, IFileSearchResult } from "@main/plugins/crossref-plugin"; 4 | 5 | interface ITagSearchProps { 6 | getSearchResults: (query:string)=>Promise<(ITagSearchResult|IFileSearchResult)[]>; 7 | handleTagClick: (event:MouseEvent)=>void; 8 | handleFileClick: (event:MouseEvent)=>void; 9 | } 10 | 11 | export const TagSearch = (props:ITagSearchProps) => { 12 | const [results, setResults] = createResource<{files:(ITagSearchResult|IFileSearchResult)[]}>( 13 | (k, getPrev) => ({ files: [] }) 14 | ); 15 | 16 | const onChange: JSX.EventHandlerUnion = async (val) => { 17 | setResults.mutate({ files: await props.getSearchResults(val.currentTarget.value) }); 18 | } 19 | 20 | return ( 21 |
22 | 23 |
24 | }> 25 | 26 | { entry => { 27 | if(entry.type == "tag-result"){ 28 | return (
32 | 33 | {(item) => { 34 | if(item.emph) { return ({item.text}); } 35 | else { return item.text; } 36 | }} 37 | 38 |
) 39 | } else if(entry.type == "file-result"){ 40 | return (
) 45 | } else { 46 | return undefined; 47 | } 48 | }} 49 | 50 | 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /noteworthy-electron/src/renderer/src/vite.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /noteworthy-electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.node.json" }, 5 | { "path": "./tsconfig.web.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /noteworthy-electron/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "electron.vite.config.*", 4 | "src/main/**/*", 5 | "src/preload/**/*", 6 | "src/common/**/*", 7 | ], 8 | "compilerOptions": { 9 | "target": "es2022", 10 | "module": "es2022", 11 | "sourceMap": false, 12 | "strict": true, 13 | "jsx": "preserve", 14 | 15 | "moduleResolution": "node", 16 | "esModuleInterop": true, 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | 20 | "composite": true, 21 | "baseUrl": ".", 22 | "paths": { 23 | "@common/*": ["src/common/*"], 24 | "@extensions/*": ["src/extensions/*"], 25 | "@main/*": ["src/main/*"], 26 | "@root/*": ["./*"], 27 | "@lib/*": ["lib/*"] 28 | }, 29 | "types": ["electron-vite/node"], 30 | // "typeRoots": ["./src/types"], 31 | "forceConsistentCasingInFileNames": true, 32 | "skipLibCheck": true, 33 | "noImplicitAny": false, 34 | "noImplicitReturns": true, 35 | "noUnusedLocals": false, 36 | "noUnusedParameters": false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /noteworthy-electron/tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/renderer/src/**/*", 4 | "src/renderer/src/**/*.tsx", 5 | "src/preload/*.d.ts", 6 | "src/common/**/*", 7 | "src/extensions/**/*", 8 | ], 9 | "compilerOptions": { 10 | "target": "esnext", 11 | "module": "esnext", 12 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 13 | "sourceMap": false, 14 | "strict": true, 15 | 16 | "jsx": "preserve", 17 | "jsxImportSource": "solid-js", 18 | 19 | "moduleResolution": "node", 20 | "esModuleInterop": true, 21 | "resolveJsonModule": true, 22 | "isolatedModules": true, 23 | 24 | "composite": true, 25 | "baseUrl": ".", 26 | "paths": { 27 | "@common/*": ["src/common/*"], 28 | "@extensions/*": ["src/extensions/*"], 29 | "@preload/*": ["src/preload/*"], 30 | "@main/*": ["src/main/*"], 31 | "@renderer/*": ["src/renderer/src/*"], 32 | "@lib/*": ["lib/*"], 33 | }, 34 | "typeRoots": ["./src/types"], 35 | 36 | "noUnusedLocals": false, 37 | "noUnusedParameters": false, 38 | "forceConsistentCasingInFileNames": true, 39 | "skipLibCheck": true, 40 | "noImplicitAny": false, 41 | "noImplicitReturns": true, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "noteworthy-electron" -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "./**/*", 5 | "../global.d.ts", 6 | "../src/**/*.d.ts" 7 | ], 8 | "compilerOptions": { 9 | "module": "commonjs", 10 | "allowJs": true, 11 | "checkJs": false, 12 | "strict": true, 13 | "rootDir": "../", 14 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 15 | "paths": { 16 | "@common/*": ["../src/common/*"], 17 | "@main/*": ["../src/main/*"], 18 | "@renderer/*": ["../src/renderer/*"], 19 | "@root/*": ["./*"], 20 | "@files/*": ["./files/*"], 21 | "@lib/*": ["lib/*"] 22 | } 23 | }, 24 | "ts-node": { "files": true }, 25 | "files": [ "../src/global.d.ts" ], 26 | } --------------------------------------------------------------------------------