Merge branch 'master' into excali-17-2

This commit is contained in:
zadam 2024-03-06 06:53:19 +01:00
commit 6fad5f2b51
56 changed files with 2802 additions and 2331 deletions

View File

@ -1,212 +1,207 @@
module.exports = { module.exports = {
env: { env: {
browser: true, browser: true,
commonjs: true, commonjs: true,
es2021: true, es2021: true,
node: true, node: true,
},
extends: ['eslint:recommended', 'airbnb-base', 'plugin:jsonc/recommended-with-jsonc'],
overrides: [
{
files: ['*.json', '*.json5', '*.jsonc'],
parser: 'jsonc-eslint-parser',
}, },
// plugins: ['prettier'], // to be activated {
extends: ['eslint:recommended', 'airbnb-base', 'plugin:jsonc/recommended-with-jsonc', 'prettier'], files: ['package.json'],
overrides: [ parser: 'jsonc-eslint-parser',
{ rules: {
files: ['*.json', '*.json5', '*.jsonc'], 'jsonc/sort-keys': [
parser: 'jsonc-eslint-parser', 'off',
}, {
{ pathPattern: '^$',
files: ['package.json'], order: [
parser: 'jsonc-eslint-parser', 'name',
rules: { 'version',
'jsonc/sort-keys': [ 'private',
'off', 'packageManager',
{ 'description',
pathPattern: '^$', 'type',
order: [ 'keywords',
'name', 'homepage',
'version', 'bugs',
'private', 'license',
'packageManager', 'author',
'description', 'contributors',
'type', 'funding',
'keywords', 'files',
'homepage', 'main',
'bugs', 'module',
'license', 'exports',
'author', 'unpkg',
'contributors', 'jsdelivr',
'funding', 'browser',
'files', 'bin',
'main', 'man',
'module', 'directories',
'exports', 'repository',
'unpkg', 'publishConfig',
'jsdelivr', 'scripts',
'browser', 'peerDependencies',
'bin', 'peerDependenciesMeta',
'man', 'optionalDependencies',
'directories', 'dependencies',
'repository', 'devDependencies',
'publishConfig', 'engines',
'scripts', 'config',
'peerDependencies', 'overrides',
'peerDependenciesMeta', 'pnpm',
'optionalDependencies', 'lint-staged',
'dependencies', 'eslintConfig',
'devDependencies', ],
'engines', },
'config', {
'overrides', pathPattern: '^(?:dev|peer|optional|bundled)?[Dd]ependencies$',
'pnpm', order: { type: 'asc' },
'husky', },
'lint-staged', ],
'eslintConfig', },
],
},
{
pathPattern: '^(?:dev|peer|optional|bundled)?[Dd]ependencies$',
order: { type: 'asc' },
},
],
},
},
],
globals: {
$: true,
jQuery: true,
glob: true,
log: true,
EditorWatchdog: true,
React: true,
appState: true,
ExcalidrawLib: true,
elements: true,
files: true,
ReactDOM: true,
// src\public\app\widgets\type_widgets\relation_map.js
jsPlumb: true,
panzoom: true,
logError: true,
// src\public\app\widgets\type_widgets\image.js
WZoom: true,
// \src\public\app\widgets\type_widgets\read_only_text.js
renderMathInElement: true,
// \src\public\app\widgets\type_widgets\editable_text.js
BalloonEditor: true,
FancytreeNode: true,
CKEditorInspector: true,
// \src\public\app\widgets\type_widgets\editable_code.js
CodeMirror: true,
// \src\public\app\services\resizer.js
Split: true,
// \src\public\app\services\content_renderer.js
mermaid: true,
// src\public\app\services\frontend_script_api.js
dayjs: true,
// \src\public\app\widgets\note_map.js
ForceGraph: true,
// \src\public\app\setup.js
ko: true,
syncInProgress: true,
// src\public\app\services\utils.js
logInfo: true,
__non_webpack_require__: true,
describe: true,
it: true,
expect: true
}, },
parserOptions: { ],
ecmaVersion: 'latest', globals: {
sourceType: 'module', $: true,
}, jQuery: true,
rules: { glob: true,
// eslint:recommended log: true,
'no-unused-vars': 'off', EditorWatchdog: true,
'linebreak-style': 'off', React: true,
'no-useless-escape': 'off', appState: true,
'no-empty': 'off', ExcalidrawLib: true,
'no-constant-condition': 'off', elements: true,
'getter-return': 'off', files: true,
'no-cond-assign': 'off', ReactDOM: true,
'no-async-promise-executor': 'off', // src\public\app\widgets\type_widgets\relation_map.js
'no-extra-semi': 'off', jsPlumb: true,
'no-inner-declarations': 'off', panzoom: true,
logError: true,
// src\public\app\widgets\type_widgets\image.js
WZoom: true,
// \src\public\app\widgets\type_widgets\read_only_text.js
renderMathInElement: true,
// \src\public\app\widgets\type_widgets\editable_text.js
BalloonEditor: true,
FancytreeNode: true,
CKEditorInspector: true,
// \src\public\app\widgets\type_widgets\editable_code.js
CodeMirror: true,
// \src\public\app\services\resizer.js
Split: true,
// \src\public\app\services\content_renderer.js
mermaid: true,
// src\public\app\services\frontend_script_api.js
dayjs: true,
// \src\public\app\widgets\note_map.js
ForceGraph: true,
// \src\public\app\setup.js
ko: true,
syncInProgress: true,
// src\public\app\services\utils.js
logInfo: true,
__non_webpack_require__: true,
describe: true,
it: true,
expect: true,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
// eslint:recommended
'no-unused-vars': 'off',
'linebreak-style': 'off',
'no-useless-escape': 'off',
'no-empty': 'off',
'no-constant-condition': 'off',
'getter-return': 'off',
'no-cond-assign': 'off',
'no-async-promise-executor': 'off',
'no-extra-semi': 'off',
'no-inner-declarations': 'off',
// prettier // airbnb-base
'prettier/prettier': ['off', { endOfLine: 'auto' }], 'no-console': 'off',
'no-plusplus': 'off',
'no-param-reassign': 'off',
'global-require': 'off',
'no-use-before-define': 'off',
'no-await-in-loop': 'off',
radix: 'off',
'import/order': 'off',
'import/no-extraneous-dependencies': 'off',
'prefer-destructuring': 'off',
'no-shadow': 'off',
'no-new': 'off',
'no-restricted-syntax': 'off',
strict: 'off',
'class-methods-use-this': 'off',
'no-else-return': 'off',
'import/no-dynamic-require': 'off',
'no-underscore-dangle': 'off',
'prefer-template': 'off',
'consistent-return': 'off',
'no-continue': 'off',
'object-shorthand': 'off',
'one-var': 'off',
'prefer-const': 'off',
'spaced-comment': 'off',
'no-loop-func': 'off',
'arrow-body-style': 'off',
// airbnb-base 'guard-for-in': 'off',
'no-console': 'off', 'no-return-assign': 'off',
'no-plusplus': 'off', 'dot-notation': 'off',
'no-param-reassign': 'off',
'global-require': 'off',
'no-use-before-define': 'off',
'no-await-in-loop': 'off',
radix: 'off',
'import/order': 'off',
'import/no-extraneous-dependencies': 'off',
'prefer-destructuring': 'off',
'no-shadow': 'off',
'no-new': 'off',
'no-restricted-syntax': 'off',
strict: 'off',
'class-methods-use-this': 'off',
'no-else-return': 'off',
'import/no-dynamic-require': 'off',
'no-underscore-dangle': 'off',
'prefer-template': 'off',
'consistent-return': 'off',
'no-continue': 'off',
'object-shorthand': 'off',
'one-var': 'off',
'prefer-const': 'off',
'spaced-comment': 'off',
'no-loop-func': 'off',
'arrow-body-style': 'off',
'guard-for-in': 'off', 'func-names': 'off',
'no-return-assign': 'off', 'import/no-useless-path-segments': 'off',
'dot-notation': 'off', 'default-param-last': 'off',
'prefer-arrow-callback': 'off',
'no-unneeded-ternary': 'off',
'no-return-await': 'off',
'import/extensions': 'off',
'func-names': 'off', 'no-var': 'off',
'import/no-useless-path-segments': 'off', 'import/newline-after-import': 'off',
'default-param-last': 'off', 'no-restricted-globals': 'off',
'prefer-arrow-callback': 'off', 'operator-assignment': 'off',
'no-unneeded-ternary': 'off', 'no-eval': 'off',
'no-return-await': 'off', 'max-classes-per-file': 'off',
'import/extensions': 'off', 'vars-on-top': 'off',
'no-bitwise': 'off',
'no-var': 'off', 'no-lonely-if': 'off',
'import/newline-after-import': 'off', 'no-multi-assign': 'off',
'no-restricted-globals': 'off', 'no-promise-executor-return': 'off',
'operator-assignment': 'off', 'no-empty-function': 'off',
'no-eval': 'off', 'import/no-unresolved': 'off',
'max-classes-per-file': 'off', camelcase: 'off',
'vars-on-top': 'off', eqeqeq: 'off',
'no-bitwise': 'off', 'lines-between-class-members': 'off',
'no-lonely-if': 'off', 'import/no-cycle': 'off',
'no-multi-assign': 'off', 'new-cap': 'off',
'no-promise-executor-return': 'off', 'prefer-object-spread': 'off',
'no-empty-function': 'off', 'no-new-func': 'off',
'import/no-unresolved': 'off', 'no-unused-expressions': 'off',
camelcase: 'off', 'lines-around-directive': 'off',
eqeqeq: 'off', 'prefer-exponentiation-operator': 'off',
'lines-between-class-members': 'off', 'no-restricted-properties': 'off',
'import/no-cycle': 'off', 'prefer-rest-params': 'off',
'new-cap': 'off', 'no-unreachable-loop': 'off',
'prefer-object-spread': 'off', 'no-alert': 'off',
'no-new-func': 'off', 'no-useless-return': 'off',
'no-unused-expressions': 'off', 'no-nested-ternary': 'off',
'lines-around-directive': 'off', 'prefer-regex-literals': 'off',
'prefer-exponentiation-operator': 'off', 'import/no-named-as-default-member': 'off',
'no-restricted-properties': 'off', yoda: 'off',
'prefer-rest-params': 'off', 'no-script-url': 'off',
'no-unreachable-loop': 'off', 'no-prototype-builtins': 'off',
'no-alert': 'off', },
'no-useless-return': 'off',
'no-nested-ternary': 'off',
'prefer-regex-literals': 'off',
'import/no-named-as-default-member': 'off',
yoda: 'off',
'no-script-url': 'off',
'no-prototype-builtins':'off'
},
}; };

1
.husky/.gitignore vendored
View File

@ -1 +0,0 @@
_

View File

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
#npx lint-staged

View File

@ -1,13 +0,0 @@
//https://prettier.io/docs/en/options.html
module.exports = {
semi: true,
trailingComma: 'none',
singleQuote: true,
printWidth: 100,
tabWidth: 4,
useTabs: false,
quoteProps: "as-needed",
bracketSpacing: true,
arrowParens: "avoid"
// htmlWhitespaceSensitivity: 'ignore',
};

View File

@ -1,6 +1,5 @@
{ {
"recommendations": [ "recommendations": [
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
] ]
} }

View File

@ -643,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU Affero General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.js.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.js.gnu.org/licenses/>. <http://www.gnu.org/licenses/>.

View File

@ -1,8 +1,14 @@
# Trilium Notes # Trilium Notes
## Trilium is in maintenance mode - see details in https://github.com/zadam/trilium/issues/4620
Preliminary disccusions on the successor organization are taking place in [Trilium Next discussions](https://github.com/orgs/TriliumNext/discussions).
[![Join the chat at https://gitter.im/trilium-notes/Lobby](https://badges.gitter.im/trilium-notes/Lobby.svg)](https://gitter.im/trilium-notes/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [English](https://github.com/zadam/trilium/blob/master/README.md) | [Chinese](https://github.com/zadam/trilium/blob/master/README-ZH_CN.md) | [Russian](https://github.com/zadam/trilium/blob/master/README.ru.md) | [Japanese](https://github.com/zadam/trilium/blob/master/README.ja.md) [![Join the chat at https://gitter.im/trilium-notes/Lobby](https://badges.gitter.im/trilium-notes/Lobby.svg)](https://gitter.im/trilium-notes/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [English](https://github.com/zadam/trilium/blob/master/README.md) | [Chinese](https://github.com/zadam/trilium/blob/master/README-ZH_CN.md) | [Russian](https://github.com/zadam/trilium/blob/master/README.ru.md) | [Japanese](https://github.com/zadam/trilium/blob/master/README.ja.md)
Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases. See [screenshots](https://github.com/zadam/trilium/wiki/Screenshot-tour) for quick overview: Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases.
See [screenshots](https://github.com/zadam/trilium/wiki/Screenshot-tour) for quick overview:
<a href="https://github.com/zadam/trilium/wiki/Screenshot-tour"><img src="https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png" alt="Trilium Screenshot" width="1000"></a> <a href="https://github.com/zadam/trilium/wiki/Screenshot-tour"><img src="https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png" alt="Trilium Screenshot" width="1000"></a>

View File

@ -2,49 +2,37 @@
SRC_DIR=./dist/trilium-linux-x64-src SRC_DIR=./dist/trilium-linux-x64-src
if [ "$1" != "DONTCOPY" ] [ "$1" != "DONTCOPY" ] && ./bin/copy-trilium.sh "$SRC_DIR"
then
./bin/copy-trilium.sh $SRC_DIR
fi
rm -r $SRC_DIR/src/public/app-dist/*.mobile.* rm -r "$SRC_DIR"/src/public/app-dist/*.mobile.*
echo "Copying required linux-x64 binaries" echo "Copying required linux-x64 binaries"
cp -r bin/better-sqlite3/linux-desktop-better_sqlite3.node "$SRC_DIR"/node_modules/better-sqlite3/build/Release/better_sqlite3.node
cp -r bin/better-sqlite3/linux-desktop-better_sqlite3.node $SRC_DIR/node_modules/better-sqlite3/build/Release/better_sqlite3.node
echo "Packaging linux x64 electron build" echo "Packaging linux x64 electron build"
./node_modules/.bin/electron-packager "$SRC_DIR" --asar --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite
./node_modules/.bin/electron-packager $SRC_DIR --asar --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite
BUILD_DIR=./dist/trilium-linux-x64 BUILD_DIR=./dist/trilium-linux-x64
rm -rf $BUILD_DIR rm -rf "$BUILD_DIR"
mv "./dist/Trilium Notes-linux-x64" $BUILD_DIR mv "./dist/Trilium Notes-linux-x64" "$BUILD_DIR"
cp images/app-icons/png/128x128.png $BUILD_DIR/icon.png cp images/app-icons/png/128x128.png "$BUILD_DIR"/icon.png
cp bin/tpl/anonymize-database.sql "$BUILD_DIR"/
cp bin/tpl/anonymize-database.sql $BUILD_DIR/ cp -r dump-db "$BUILD_DIR"/
rm -rf "$BUILD_DIR"/dump-db/node_modules
cp -r dump-db $BUILD_DIR/ for f in 'trilium-portable' 'trilium-safe-mode' 'trilium-no-cert-check'; do
rm -rf $BUILD_DIR/dump-db/node_modules cp bin/tpl/"$f".sh "$BUILD_DIR"/
chmod 755 "$BUILD_DIR"/"$f".sh
cp bin/tpl/trilium-portable.sh $BUILD_DIR/ done
chmod 755 $BUILD_DIR/trilium-portable.sh
cp bin/tpl/trilium-safe-mode.sh $BUILD_DIR/
chmod 755 $BUILD_DIR/trilium-safe-mode.sh
cp bin/tpl/trilium-no-cert-check.sh $BUILD_DIR/
chmod 755 $BUILD_DIR/trilium-no-cert-check.sh
echo "Packaging linux x64 electron distribution..." echo "Packaging linux x64 electron distribution..."
VERSION=`jq -r ".version" package.json` VERSION=`jq -r ".version" package.json`
cd dist pushd dist
tar cJf "trilium-linux-x64-${VERSION}.tar.xz" trilium-linux-x64
tar cJf trilium-linux-x64-${VERSION}.tar.xz trilium-linux-x64 popd
cd ..
bin/build-debian.sh bin/build-debian.sh

View File

@ -4,47 +4,49 @@ if [[ $# -eq 0 ]] ; then
echo "Missing argument of target directory" echo "Missing argument of target directory"
exit 1 exit 1
fi fi
if ! [[ $(which npm) ]]; then
echo "Missing npm"
exit 1
fi
n exec 18.18.2 npm run webpack n exec 18.18.2 npm run webpack || npm run webpack
DIR=$1 DIR="$1"
rm -rf $DIR rm -rf "$DIR"
mkdir $DIR mkdir -pv "$DIR"
echo "Copying Trilium to build directory $DIR" echo "Copying Trilium to build directory $DIR"
cp -r images $DIR/ for d in 'images' 'libraries' 'src' 'db'; do
cp -r libraries $DIR/ cp -r "$d" "$DIR"/
cp -r src $DIR/ done
cp -r db $DIR/ for f in 'package.json' 'package-lock.json' 'README.md' 'LICENSE' 'config-sample.ini' 'electron.js'; do
cp -r package.json $DIR/ cp "$f" "$DIR"/
cp -r package-lock.json $DIR/ done
cp -r README.md $DIR/ cp webpack-* "$DIR"/ # here warning because there is no 'webpack-*', but webpack.config.js only
cp -r LICENSE $DIR/
cp -r config-sample.ini $DIR/
cp -r electron.js $DIR/
cp webpack-* $DIR/
# run in subshell (so we return to original dir) # run in subshell (so we return to original dir)
(cd $DIR && n exec 18.18.2 npm install --only=prod) (cd $DIR && n exec 18.18.2 npm install --only=prod)
if [[ -d "$DIR"/node_modules ]]; then
# cleanup of useless files in dependencies # cleanup of useless files in dependencies
rm -r $DIR/node_modules/image-q/demo for d in 'image-q/demo' 'better-sqlite3/Release' 'better-sqlite3/deps/sqlite3.tar.gz' '@jimp/plugin-print/fonts' 'jimp/browser' 'jimp/fonts'; do
rm -r $DIR/node_modules/better-sqlite3/Release [[ -e "$DIR"/node_modules/"$d" ]] && rm -rv "$DIR"/node_modules/"$d"
rm -r $DIR/node_modules/better-sqlite3/deps/sqlite3.tar.gz done
rm -r $DIR/node_modules/@jimp/plugin-print/fonts
rm -r $DIR/node_modules/jimp/browser
rm -r $DIR/node_modules/jimp/fonts
# delete all tests (there are often large images as test file for jimp etc.) # delete all tests (there are often large images as test file for jimp etc.)
find $DIR/node_modules -name test -exec rm -rf {} \; for d in 'test' 'docs' 'demo'; do
find $DIR/node_modules -name docs -exec rm -rf {} \; find "$DIR"/node_modules -name "$d" -exec rm -rf {} \;
find $DIR/node_modules -name demo -exec rm -rf {} \; done
fi
find $DIR/libraries -name "*.map" -type f -delete find $DIR/libraries -name "*.map" -type f -delete
cp $DIR/src/public/app/share.js $DIR/src/public/app-dist/ d="$DIR"/src/public
cp -r $DIR/src/public/app/doc_notes $DIR/src/public/app-dist/ [[ -d "$d"/app-dist ]] || mkdir -pv "$d"/app-dist
cp "$d"/app/share.js "$d"/app-dist/
cp -r "$d"/app/doc_notes "$d"/app-dist/
rm -rf $DIR/src/public/app rm -rf "$d"/app
unset f d DIR

View File

@ -8,3 +8,6 @@ CREATE TABLE IF NOT EXISTS "blobs" (
ALTER TABLE notes ADD blobId TEXT DEFAULT NULL; ALTER TABLE notes ADD blobId TEXT DEFAULT NULL;
ALTER TABLE note_revisions ADD blobId TEXT DEFAULT NULL; ALTER TABLE note_revisions ADD blobId TEXT DEFAULT NULL;
CREATE INDEX IF NOT EXISTS IDX_notes_blobId on notes (blobId);
CREATE INDEX IF NOT EXISTS IDX_note_revisions_blobId on note_revisions (blobId);

View File

@ -21,5 +21,6 @@ CREATE INDEX `IDX_revisions_utcDateCreated` ON `revisions` (`utcDateCreated`);
CREATE INDEX `IDX_revisions_utcDateLastEdited` ON `revisions` (`utcDateLastEdited`); CREATE INDEX `IDX_revisions_utcDateLastEdited` ON `revisions` (`utcDateLastEdited`);
CREATE INDEX `IDX_revisions_dateCreated` ON `revisions` (`dateCreated`); CREATE INDEX `IDX_revisions_dateCreated` ON `revisions` (`dateCreated`);
CREATE INDEX `IDX_revisions_dateLastEdited` ON `revisions` (`dateLastEdited`); CREATE INDEX `IDX_revisions_dateLastEdited` ON `revisions` (`dateLastEdited`);
CREATE INDEX IF NOT EXISTS IDX_revisions_blobId on revisions (blobId);
UPDATE entity_changes SET entityName = 'revisions' WHERE entityName = 'note_revisions'; UPDATE entity_changes SET entityName = 'revisions' WHERE entityName = 'note_revisions';

View File

@ -19,3 +19,5 @@ CREATE INDEX IDX_attachments_ownerId_role
CREATE INDEX IDX_attachments_utcDateScheduledForErasureSince CREATE INDEX IDX_attachments_utcDateScheduledForErasureSince
on attachments (utcDateScheduledForErasureSince); on attachments (utcDateScheduledForErasureSince);
CREATE INDEX IF NOT EXISTS IDX_attachments_blobId on attachments (blobId);

View File

@ -0,0 +1,17 @@
-- + is normally replaced by X and / by Y, but this can temporarily cause UNIQUE key exception
-- this might create blob duplicates, but cleanup will eventually take care of it
UPDATE blobs SET blobId = REPLACE(blobId, '+', 'A');
UPDATE blobs SET blobId = REPLACE(blobId, '/', 'B');
UPDATE notes SET blobId = REPLACE(blobId, '+', 'A');
UPDATE notes SET blobId = REPLACE(blobId, '/', 'B');
UPDATE attachments SET blobId = REPLACE(blobId, '+', 'A');
UPDATE attachments SET blobId = REPLACE(blobId, '/', 'B');
UPDATE revisions SET blobId = REPLACE(blobId, '+', 'A');
UPDATE revisions SET blobId = REPLACE(blobId, '/', 'B');
UPDATE entity_changes SET entityId = REPLACE(entityId, '+', 'A') WHERE entityName = 'blobs';
UPDATE entity_changes SET entityId = REPLACE(entityId, '/', 'B') WHERE entityName = 'blobs';

View File

@ -5,8 +5,8 @@
} }
/* /*
* CKEditor 5 (v40.1.0) content styles. * CKEditor 5 (v41.0.0) content styles.
* Generated on Mon, 20 Nov 2023 08:41:49 GMT. * Generated on Fri, 26 Jan 2024 10:23:49 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html
*/ */
@ -42,18 +42,6 @@
overflow-wrap: break-word; overflow-wrap: break-word;
position: relative; position: relative;
} }
/* @ckeditor/ckeditor5-table/theme/tablecaption.css */
.ck-content .table > figcaption {
display: table-caption;
caption-side: top;
word-break: break-word;
text-align: center;
color: var(--ck-color-selector-caption-text);
background-color: var(--ck-color-selector-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* @ckeditor/ckeditor5-table/theme/table.css */ /* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table { .ck-content .table {
margin: 0.9em auto; margin: 0.9em auto;
@ -87,12 +75,17 @@
.ck-content[dir="ltr"] .table th { .ck-content[dir="ltr"] .table th {
text-align: left; text-align: left;
} }
/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */ /* @ckeditor/ckeditor5-table/theme/tablecaption.css */
.ck-content .media { .ck-content .table > figcaption {
clear: both; display: table-caption;
margin: 0.9em 0; caption-side: top;
display: block; word-break: break-word;
min-width: 15em; text-align: center;
color: var(--ck-color-selector-caption-text);
background-color: var(--ck-color-selector-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
} }
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break { .ck-content .page-break {
@ -130,6 +123,13 @@
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 0.9em 0;
display: block;
min-width: 15em;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */ /* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list { .ck-content .todo-list {
list-style: none; list-style: none;
@ -280,6 +280,42 @@
.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { .ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
position: absolute; position: absolute;
} }
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol {
list-style-type: decimal;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol {
list-style-type: lower-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol {
list-style-type: lower-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol {
list-style-type: upper-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol ol {
list-style-type: upper-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul {
list-style-type: disc;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul {
list-style-type: circle;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-image/theme/image.css */ /* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image { .ck-content .image {
display: table; display: table;
@ -318,17 +354,6 @@
flex-shrink: 1; flex-shrink: 1;
max-width: 100%; max-width: 100%;
} }
/* @ckeditor/ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: var(--ck-color-image-caption-text);
background-color: var(--ck-color-image-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */ /* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content img.image_resized { .ck-content img.image_resized {
height: auto; height: auto;
@ -347,67 +372,16 @@
.ck-content .image.image_resized > figcaption { .ck-content .image.image_resized > figcaption {
display: block; display: block;
} }
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ /* @ckeditor/ckeditor5-image/theme/imagecaption.css */
.ck-content .marker-yellow { .ck-content .image > figcaption {
background-color: var(--ck-highlight-marker-yellow); display: table-caption;
} caption-side: bottom;
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ word-break: break-word;
.ck-content .marker-green { color: var(--ck-color-image-caption-text);
background-color: var(--ck-highlight-marker-green); background-color: var(--ck-color-image-caption-background);
} padding: .6em;
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ font-size: .75em;
.ck-content .marker-pink { outline-offset: -1px;
background-color: var(--ck-highlight-marker-pink);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol {
list-style-type: decimal;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol {
list-style-type: lower-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol {
list-style-type: lower-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol {
list-style-type: upper-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol ol {
list-style-type: upper-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul {
list-style-type: disc;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul {
list-style-type: circle;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul ul {
list-style-type: square;
} }
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ /* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left, .ck-content .image-style-block-align-left,
@ -470,6 +444,32 @@
.ck-content .image-inline.image-style-align-right { .ck-content .image-inline.image-style-align-right {
margin-left: var(--ck-inline-image-style-spacing); margin-left: var(--ck-inline-image-style-spacing);
} }
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */ /* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote { .ck-content blockquote {
overflow: hidden; overflow: hidden;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3770
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.62.4", "version": "0.63.3",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@ -32,19 +32,18 @@
"test-es6": "node -r esm spec-es6/attribute_parser.spec.js ", "test-es6": "node -r esm spec-es6/attribute_parser.spec.js ",
"test": "npm run test-jasmine && npm run test-es6", "test": "npm run test-jasmine && npm run test-es6",
"postinstall": "rimraf ./node_modules/canvas", "postinstall": "rimraf ./node_modules/canvas",
"lint": "eslint . --cache", "lint": "eslint . --cache"
"prepare": "husky install || echo 'Husky install failed, expected on flatpak build'"
}, },
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "6.0.4", "@braintree/sanitize-url": "6.0.4",
"@electron/remote": "2.1.1", "@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.16.1", "@excalidraw/excalidraw": "0.16.1",
"archiver": "6.0.1", "archiver": "7.0.0",
"async-mutex": "0.4.0", "async-mutex": "0.4.1",
"axios": "1.6.3", "axios": "1.6.7",
"better-sqlite3": "8.4.0", "better-sqlite3": "8.4.0",
"boxicons": "2.1.4", "boxicons": "2.1.4",
"chokidar": "3.5.3", "chokidar": "3.6.0",
"cls-hooked": "4.2.2", "cls-hooked": "4.2.2",
"compression": "1.7.4", "compression": "1.7.4",
"cookie-parser": "1.4.6", "cookie-parser": "1.4.6",
@ -54,35 +53,35 @@
"debounce": "1.2.1", "debounce": "1.2.1",
"ejs": "3.1.9", "ejs": "3.1.9",
"electron-debug": "3.2.0", "electron-debug": "3.2.0",
"electron-dl": "3.5.1", "electron-dl": "3.5.2",
"electron-window-state": "5.0.3", "electron-window-state": "5.0.3",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"express": "4.18.2", "express": "4.18.3",
"express-partial-content": "1.0.2", "express-partial-content": "1.0.2",
"express-rate-limit": "7.1.5", "express-rate-limit": "7.2.0",
"express-session": "1.17.3", "express-session": "1.18.0",
"force-graph": "1.43.4", "force-graph": "1.43.5",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"helmet": "7.1.0", "helmet": "7.1.0",
"html": "1.0.0", "html": "1.0.0",
"html2plaintext": "2.1.4", "html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.0", "http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.4",
"image-type": "4.1.0", "image-type": "4.1.0",
"ini": "3.0.1", "ini": "3.0.1",
"is-animated": "2.0.2", "is-animated": "2.0.2",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"jimp": "0.22.10", "jimp": "0.22.12",
"joplin-turndown-plugin-gfm": "1.0.12", "joplin-turndown-plugin-gfm": "1.0.12",
"jquery": "3.7.1", "jquery": "3.7.1",
"jquery-hotkeys": "0.2.2", "jquery-hotkeys": "0.2.2",
"jsdom": "23.0.1", "jsdom": "24.0.0",
"katex": "0.16.9", "katex": "0.16.9",
"marked": "9.1.6", "marked": "12.0.0",
"mermaid": "10.6.1", "mermaid": "10.9.0",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"node-abi": "3.52.0", "node-abi": "3.56.0",
"normalize-strings": "1.1.1", "normalize-strings": "1.1.1",
"open": "8.4.1", "open": "8.4.1",
"panzoom": "9.4.3", "panzoom": "9.4.3",
@ -94,45 +93,41 @@
"rimraf": "5.0.5", "rimraf": "5.0.5",
"safe-compare": "1.1.4", "safe-compare": "1.1.4",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"sanitize-html": "2.11.0", "sanitize-html": "2.12.1",
"sax": "1.3.0", "sax": "1.3.0",
"semver": "7.5.4", "semver": "7.6.0",
"serve-favicon": "2.5.0", "serve-favicon": "2.5.0",
"session-file-store": "1.5.0", "session-file-store": "1.5.0",
"split.js": "1.6.5", "split.js": "1.6.5",
"stream-throttle": "0.1.3", "stream-throttle": "0.1.3",
"striptags": "3.2.0", "striptags": "3.2.0",
"tmp": "0.2.1", "tmp": "0.2.3",
"tree-kill": "1.2.2", "tree-kill": "1.2.2",
"turndown": "7.1.2", "turndown": "7.1.2",
"unescape": "1.0.1", "unescape": "1.0.1",
"ws": "8.16.0", "ws": "8.16.0",
"xml2js": "0.6.2", "xml2js": "0.6.2",
"yauzl": "2.10.0" "yauzl": "3.1.2"
}, },
"devDependencies": { "devDependencies": {
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron": "28.1.0", "electron": "25.9.8",
"electron-builder": "24.9.1", "electron-builder": "24.13.3",
"electron-packager": "17.1.2", "electron-packager": "17.1.2",
"electron-rebuild": "3.2.9", "electron-rebuild": "3.2.9",
"eslint": "8.56.0", "eslint": "8.57.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-jsonc": "2.11.2", "eslint-plugin-jsonc": "2.13.0",
"eslint-plugin-prettier": "5.0.1",
"esm": "3.2.25", "esm": "3.2.25",
"husky": "8.0.3",
"jasmine": "5.1.0", "jasmine": "5.1.0",
"jsdoc": "4.0.2", "jsdoc": "4.0.2",
"jsonc-eslint-parser": "2.4.0", "jsonc-eslint-parser": "2.4.0",
"lint-staged": "15.2.0", "lint-staged": "15.2.2",
"lorem-ipsum": "2.0.8", "lorem-ipsum": "2.0.8",
"nodemon": "3.0.2", "nodemon": "3.1.0",
"prettier": "3.1.1",
"rcedit": "4.0.1", "rcedit": "4.0.1",
"webpack": "5.89.0", "webpack": "5.90.3",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -161,6 +161,13 @@ class BRevision extends AbstractBeccaEntity {
return this.getAttachments().filter(attachment => attachment.title === title)[0]; return this.getAttachments().filter(attachment => attachment.title === title)[0];
} }
/**
* Revisions are not soft-deletable, they are immediately hard-deleted (erased).
*/
eraseRevision() {
require("../../services/erase.js").eraseRevisions([this.revisionId]);
}
beforeSaving() { beforeSaving() {
super.beforeSaving(); super.beforeSaving();

View File

@ -427,6 +427,116 @@ paths:
application/json; charset=utf-8: application/json; charset=utf-8:
schema: schema:
$ref: '#/components/schemas/Error' $ref: '#/components/schemas/Error'
/attachments:
post:
description: create an attachment
operationId: postAttachment
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateAttachment'
responses:
'201':
description: attachment created
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Attachment'
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
/attachments/{attachmentId}:
parameters:
- name: attachmentId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
get:
description: Returns an attachment identified by its ID
operationId: getAttachmentById
responses:
'200':
description: attachment response
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Attachment'
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
patch:
description: patch an attachment identified by the attachmentId with changes in the body. Only role, mime, title, and position are patchable.
operationId: patchAttachmentById
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Attachment'
responses:
'200':
description: attribute updated
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Attachment'
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
delete:
description: deletes an attachment based on the attachmentId supplied.
operationId: deleteAttachmentById
responses:
'204':
description: attachment deleted
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: '#/components/schemas/Error'
/attachments/{attachmentId}/content:
parameters:
- name: attachmentId
in: path
required: true
schema:
$ref: '#/components/schemas/EntityId'
get:
description: Returns attachment content identified by its ID
operationId: getAttachmentContent
responses:
'200':
description: attachment content response
content:
text/html:
schema:
type: string
put:
description: Updates attachment content identified by its ID
operationId: putAttachmentContentById
requestBody:
description: html content of attachment
required: true
content:
text/plain:
schema:
type: string
responses:
'204':
description: attachment content updated
/attributes: /attributes:
post: post:
description: create an attribute for a given note description: create an attribute for a given note
@ -474,7 +584,7 @@ paths:
schema: schema:
$ref: '#/components/schemas/Error' $ref: '#/components/schemas/Error'
patch: patch:
description: patch a attribute identified by the attributeId with changes in the body. For labels, only value and position can be updated. For relations, only position can be updated. If you want to modify other properties, you need to delete the old attribute and create a new one. description: patch an attribute identified by the attributeId with changes in the body. For labels, only value and position can be updated. For relations, only position can be updated. If you want to modify other properties, you need to delete the old attribute and create a new one.
operationId: patchAttributeById operationId: patchAttributeById
requestBody: requestBody:
required: true required: true
@ -496,7 +606,7 @@ paths:
schema: schema:
$ref: '#/components/schemas/Error' $ref: '#/components/schemas/Error'
delete: delete:
description: deletes a attribute based on the attributeId supplied. description: deletes an attribute based on the attributeId supplied.
operationId: deleteAttributeById operationId: deleteAttributeById
responses: responses:
'204': '204':
@ -884,6 +994,57 @@ components:
$ref: '#/components/schemas/Note' $ref: '#/components/schemas/Note'
branch: branch:
$ref: '#/components/schemas/Branch' $ref: '#/components/schemas/Branch'
Attachment:
type: object
description: Attachment is owned by a note, has title and content
properties:
attachmentId:
$ref: '#/components/schemas/EntityId'
readOnly: true
ownerId:
$ref: '#/components/schemas/EntityId'
description: identifies the owner of the attachment, is either noteId or revisionId
role:
type: string
mime:
type: string
title:
type: string
position:
type: integer
format: int32
blobId:
type: string
description: ID of the blob object which effectively serves as a content hash
dateModified:
$ref: '#/components/schemas/LocalDateTime'
readOnly: true
utcDateModified:
$ref: '#/components/schemas/UtcDateTime'
readOnly: true
utcDateScheduledForErasureSince:
$ref: '#/components/schemas/UtcDateTime'
readOnly: true
contentLength:
type: integer
format: int32
CreateAttachment:
type: object
properties:
ownerId:
$ref: '#/components/schemas/EntityId'
description: identifies the owner of the attachment, is either noteId or revisionId
role:
type: string
mime:
type: string
title:
type: string
content:
type: string
position:
type: integer
format: int32
Attribute: Attribute:
type: object type: object
description: Attribute (Label, Relation) is a key-value record attached to a note. description: Attribute (Label, Relation) is a key-value record attached to a note.

View File

@ -0,0 +1,44 @@
import utils from "../services/utils.js";
import contextMenu from "./context_menu.js";
import imageService from "../services/image.js";
const PROP_NAME = "imageContextMenuInstalled";
function setupContextMenu($image) {
if (!utils.isElectron() || $image.prop(PROP_NAME)) {
return;
}
$image.prop(PROP_NAME, true);
$image.on('contextmenu', e => {
e.preventDefault();
contextMenu.show({
x: e.pageX,
y: e.pageY,
items: [
{
title: "Copy reference to clipboard",
command: "copyImageReferenceToClipboard",
uiIcon: "bx bx-empty"
},
{title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty"},
],
selectMenuItemHandler: ({command}) => {
if (command === 'copyImageReferenceToClipboard') {
imageService.copyImageReferenceToClipboard($image);
} else if (command === 'copyImageToClipboard') {
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
utils.dynamicRequire('electron');
webContents.copyImageAt(e.pageX, e.pageY);
} else {
throw new Error(`Unrecognized command '${command}'`);
}
}
});
});
}
export default {
setupContextMenu
};

View File

@ -107,6 +107,13 @@ async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
return false; return false;
} }
try {
await activateParentNotePath();
}
catch (e) {
console.error(e);
}
const taskId = utils.randomString(10); const taskId = utils.randomString(10);
let counter = 0; let counter = 0;
@ -134,6 +141,16 @@ async function deleteNotes(branchIdsToDelete, forceDeleteAllClones = false) {
return true; return true;
} }
async function activateParentNotePath() {
// this is not perfect, maybe we should find the next/previous sibling, but that's more complex
const activeContext = appContext.tabManager.getActiveContext();
const parentNotePathArr = activeContext.notePathArray.slice(0, -1);
if (parentNotePathArr.length > 0) {
activeContext.setNote(parentNotePathArr.join("/"));
}
}
async function moveNodeUpInHierarchy(node) { async function moveNodeUpInHierarchy(node) {
if (hoistedNoteService.isHoistedNode(node) if (hoistedNoteService.isHoistedNode(node)
|| hoistedNoteService.isTopLevelNode(node) || hoistedNoteService.isTopLevelNode(node)

View File

@ -9,6 +9,7 @@ import linkService from "./link.js";
import treeService from "./tree.js"; import treeService from "./tree.js";
import FNote from "../entities/fnote.js"; import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js"; import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js";
let idCounter = 1; let idCounter = 1;
@ -148,6 +149,8 @@ function renderImage(entity, $renderedContent, options = {}) {
}); });
}); });
} }
imageContextMenuService.setupContextMenu($img);
} }
function renderFile(entity, type, $renderedContent) { function renderFile(entity, type, $renderedContent) {

View File

@ -48,11 +48,11 @@ async function checkNoteAccess(notePath, noteContext) {
const hoistedNoteId = noteContext.hoistedNoteId; const hoistedNoteId = noteContext.hoistedNoteId;
if (!resolvedNotePath.includes(hoistedNoteId) && !resolvedNotePath.includes('_hidden')) { if (!resolvedNotePath.includes(hoistedNoteId) && (!resolvedNotePath.includes('_hidden') || resolvedNotePath.includes('_lbBookmarks'))) {
const requestedNote = await froca.getNote(treeService.getNoteIdFromUrl(resolvedNotePath)); const requestedNote = await froca.getNote(treeService.getNoteIdFromUrl(resolvedNotePath));
const hoistedNote = await froca.getNote(hoistedNoteId); const hoistedNote = await froca.getNote(hoistedNoteId);
if (!hoistedNote.hasAncestor('_hidden') if ((!hoistedNote.hasAncestor('_hidden') || resolvedNotePath.includes('_lbBookmarks'))
&& !await dialogService.confirm(`Requested note '${requestedNote.title}' is outside of hoisted note '${hoistedNote.title}' subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?`)) { && !await dialogService.confirm(`Requested note '${requestedNote.title}' is outside of hoisted note '${hoistedNote.title}' subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?`)) {
return false; return false;
} }

View File

@ -4,6 +4,7 @@ import froca from "./froca.js";
import attributeRenderer from "./attribute_renderer.js"; import attributeRenderer from "./attribute_renderer.js";
import libraryLoader from "./library_loader.js"; import libraryLoader from "./library_loader.js";
import treeService from "./tree.js"; import treeService from "./tree.js";
import utils from "./utils.js";
const TPL = ` const TPL = `
<div class="note-list"> <div class="note-list">
@ -215,7 +216,11 @@ class NoteListRenderer {
if (highlightedTokens.length > 0) { if (highlightedTokens.length > 0) {
await libraryLoader.requireLibrary(libraryLoader.MARKJS); await libraryLoader.requireLibrary(libraryLoader.MARKJS);
this.highlightRegex = new RegExp(highlightedTokens.join("|"), 'gi'); const regex = highlightedTokens
.map(token => utils.escapeRegExp(token))
.join("|");
this.highlightRegex = new RegExp(regex, 'gi');
} else { } else {
this.highlightRegex = null; this.highlightRegex = null;
} }

View File

@ -200,7 +200,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
this.attributeDetailWidget.hide(); this.attributeDetailWidget.hide();
}); });
this.$editor.on('blur', () => this.save()); this.$editor.on('blur', () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160
this.$addNewAttributeButton = this.$widget.find('.add-new-attribute-button'); this.$addNewAttributeButton = this.$widget.find('.add-new-attribute-button');
this.$addNewAttributeButton.on('click', e => this.addNewAttribute(e)); this.$addNewAttributeButton.on('click', e => this.addNewAttribute(e));

View File

@ -8,10 +8,13 @@ export default class RightPaneContainer extends FlexContainer {
this.id('right-pane'); this.id('right-pane');
this.css('height', '100%'); this.css('height', '100%');
this.collapsible(); this.collapsible();
this.rightPaneHidden = false;
} }
isEnabled() { isEnabled() {
return super.isEnabled() return super.isEnabled()
&& !this.rightPaneHidden
&& this.children.length > 0 && this.children.length > 0
&& !!this.children.find(ch => ch.isEnabled() && ch.canBeShown()); && !!this.children.find(ch => ch.isEnabled() && ch.canBeShown());
} }
@ -44,4 +47,10 @@ export default class RightPaneContainer extends FlexContainer {
splitService.setupRightPaneResizer(); splitService.setupRightPaneResizer();
} }
} }
toggleRightPaneEvent() {
this.rightPaneHidden = !this.rightPaneHidden;
this.reEvaluateRightPaneVisibilityCommand();
}
} }

View File

@ -1091,7 +1091,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return; return;
} }
const nodeCtx = this.#getActiveNodeCtx(); const activeNode = this.getActiveNode();
const activeNodeFocused = activeNode?.hasFocus();
const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const refreshCtx = { const refreshCtx = {
noteIdsToUpdate: new Set(), noteIdsToUpdate: new Set(),
@ -1108,7 +1110,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
await this.#executeTreeUpdates(refreshCtx, loadResults); await this.#executeTreeUpdates(refreshCtx, loadResults);
await this.#setActiveNode(nodeCtx, movedActiveNode, parentsOfAddedNodes); await this.#setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes);
if (refreshCtx.noteIdsToReload.size > 0 || refreshCtx.noteIdsToUpdate.size > 0) { if (refreshCtx.noteIdsToReload.size > 0 || refreshCtx.noteIdsToUpdate.size > 0) {
// workaround for https://github.com/mar10/fancytree/issues/1054 // workaround for https://github.com/mar10/fancytree/issues/1054
@ -1257,73 +1259,42 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} }
} }
#getActiveNodeCtx() { async #setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes) {
const nodeCtx = {
activeNotePath: null,
activeNodeFocused: null,
nextNotePath: null
};
const activeNode = this.getActiveNode();
nodeCtx.activeNodeFocused = activeNode?.hasFocus();
nodeCtx.activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null;
const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null;
nodeCtx.nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null;
return nodeCtx;
}
async #setActiveNode(nodeCtx, movedActiveNode, parentsOfAddedNodes) {
if (movedActiveNode) { if (movedActiveNode) {
for (const parentNode of parentsOfAddedNodes) { for (const parentNode of parentsOfAddedNodes) {
const foundNode = (parentNode.getChildren() || []).find(child => child.data.noteId === movedActiveNode.data.noteId); const foundNode = (parentNode.getChildren() || []).find(child => child.data.noteId === movedActiveNode.data.noteId);
if (foundNode) { if (foundNode) {
nodeCtx.activeNotePath = treeService.getNotePath(foundNode); activeNotePath = treeService.getNotePath(foundNode);
break; break;
} }
} }
} }
if (!nodeCtx.activeNotePath) { if (!activeNotePath) {
return; return;
} }
let node = await this.expandToNote(nodeCtx.activeNotePath, false); let node = await this.expandToNote(activeNotePath, false);
if (node && node.data.noteId !== treeService.getNoteIdFromUrl(nodeCtx.activeNotePath)) {
if (node && node.data.noteId !== treeService.getNoteIdFromUrl(activeNotePath)) {
// if the active note has been moved elsewhere then it won't be found by the path, // if the active note has been moved elsewhere then it won't be found by the path,
// so we switch to the alternative of trying to find it by noteId // so we switch to the alternative of trying to find it by noteId
const notesById = this.getNodesByNoteId(treeService.getNoteIdFromUrl(nodeCtx.activeNotePath)); const notesById = this.getNodesByNoteId(treeService.getNoteIdFromUrl(activeNotePath));
// if there are multiple clones, then we'd rather not activate anyone // if there are multiple clones, then we'd rather not activate anyone
node = notesById.length === 1 ? notesById[0] : null; node = notesById.length === 1 ? notesById[0] : null;
} }
if (node) { if (!node) {
if (nodeCtx.activeNodeFocused) { return;
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
this.tree.$container.focus();
}
await node.setActive(true, {noEvents: true, noFocus: !nodeCtx.activeNodeFocused});
} else {
// this is used when the original note has been deleted, and we want to move the focus to the note above/below
node = await this.expandToNote(nodeCtx.nextNotePath, false);
if (node) {
// FIXME: this is conceptually wrong
// here note tree is responsible for updating global state of the application
// this should be done by NoteContext / TabManager and note tree should only listen to
// changes in active note and just set the "active" state
// We don't await since that can bring up infinite cycles when e.g. custom widget does some backend requests which wait for max sync ID processed
appContext.tabManager.getActiveContext().setNote(nodeCtx.nextNotePath).then(() => {
const newActiveNode = this.getActiveNode();
// return focus if the previously active node was also focused
if (newActiveNode && nodeCtx.activeNodeFocused) {
newActiveNode.setFocus(true);
}
});
}
} }
if (activeNodeFocused) {
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
this.tree.$container.focus();
}
await node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused});
} }
sortChildren(node) { sortChildren(node) {

View File

@ -30,9 +30,14 @@ const TPL = `
height: 40px; height: 40px;
width: 40px; width: 40px;
} }
.title-bar-buttons .top-btn.active{ .title-bar-buttons .top-btn.active{
background-color:var(--accented-background-color); background-color:var(--accented-background-color);
} }
.title-bar-buttons .btn.focus, .title-bar-buttons .btn:focus {
box-shadow: none;
}
</style> </style>
<!-- divs act as a hitbox for the buttons, making them clickable on corners --> <!-- divs act as a hitbox for the buttons, making them clickable on corners -->

View File

@ -1,7 +1,7 @@
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import TypeWidget from "./type_widget.js"; import TypeWidget from "./type_widget.js";
import libraryLoader from "../../services/library_loader.js"; import libraryLoader from "../../services/library_loader.js";
import contextMenu from "../../menus/context_menu.js"; import imageContextMenuService from "../../menus/image_context_menu.js";
import imageService from "../../services/image.js"; import imageService from "../../services/image.js";
const TPL = ` const TPL = `
@ -55,36 +55,7 @@ class ImageTypeWidget extends TypeWidget {
}); });
}); });
if (utils.isElectron()) { imageContextMenuService.setupContextMenu(this.$imageView);
// for browser, we want to let the native menu
this.$imageView.on('contextmenu', e => {
e.preventDefault();
contextMenu.show({
x: e.pageX,
y: e.pageY,
items: [
{
title: "Copy reference to clipboard",
command: "copyImageReferenceToClipboard",
uiIcon: "bx bx-empty"
},
{title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty"},
],
selectMenuItemHandler: ({command}) => {
if (command === 'copyImageReferenceToClipboard') {
imageService.copyImageReferenceToClipboard(this.$imageWrapper);
} else if (command === 'copyImageToClipboard') {
const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
utils.dynamicRequire('electron');
webContents.copyImageAt(e.pageX, e.pageY);
} else {
throw new Error(`Unrecognized command '${command}'`);
}
}
});
});
}
super.doRender(); super.doRender();
} }

View File

@ -18,6 +18,8 @@ const TPL = `
<h5>Highlights List visibility</h5> <h5>Highlights List visibility</h5>
<p>You can hide the highlights widget per-note by adding a <code>#hideHighlightWidget</code> label.</p> <p>You can hide the highlights widget per-note by adding a <code>#hideHighlightWidget</code> label.</p>
<p>You can configure a keyboard shortcut for quickly toggling the right pane (including Highlights) in the Options -> Shortcuts (name "toggleRightPane").</p>
</div>`; </div>`;
export default class HighlightsListOptions extends OptionsWidget { export default class HighlightsListOptions extends OptionsWidget {

View File

@ -11,6 +11,8 @@ const TPL = `
</div> </div>
<p>You can also use this option to effectively disable TOC by setting a very high number.</p> <p>You can also use this option to effectively disable TOC by setting a very high number.</p>
<p>You can configure a keyboard shortcut for quickly toggling the right pane (including TOC) in the Options -> Shortcuts (name "toggleRightPane").</p>
</div>`; </div>`;
export default class TableOfContentsOptions extends OptionsWidget { export default class TableOfContentsOptions extends OptionsWidget {

View File

@ -9,14 +9,9 @@
"start_url": "/", "start_url": "/",
"icons": [ "icons": [
{ {
"src": "/assets/vX/images/app-icons/ios/apple-touch-icon.png", "src": "favicon.ico",
"sizes": "180x180", "sizes": "180x180 512x512",
"type": "image/png" "type": "image/x-icon"
},
{
"src": "/assets/vX/images/app-icons/png/512x512.png",
"sizes": "512x512",
"type": "image/png"
} }
] ]
} }

View File

@ -3,6 +3,7 @@ body {
--ck-color-base-text: var(--main-text-color); --ck-color-base-text: var(--main-text-color);
--ck-color-base-foreground: var(--accented-background-color); --ck-color-base-foreground: var(--accented-background-color);
--ck-color-base-background: var(--main-background-color);
--ck-color-focus-border: var(--main-border-color); --ck-color-focus-border: var(--main-border-color);
--ck-color-text: var(--main-text-color); --ck-color-text: var(--main-text-color);
--ck-color-shadow-drop: var(--main-background-color); --ck-color-shadow-drop: var(--main-background-color);

View File

@ -88,3 +88,7 @@ body .CodeMirror {
.excalidraw.theme--dark { .excalidraw.theme--dark {
--theme-filter: invert(80%) hue-rotate(180deg) !important; --theme-filter: invert(80%) hue-rotate(180deg) !important;
} }
body .todo-list input[type="checkbox"]:not(:checked):before {
border-color: var(--muted-text-color) !important;
}

View File

@ -39,7 +39,13 @@ function getLightAnonymizationScript() {
SELECT blobId FROM notes WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend') SELECT blobId FROM notes WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend')
UNION ALL UNION ALL
SELECT blobId FROM revisions WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend') SELECT blobId FROM revisions WHERE mime IN ('application/javascript;env=backend', 'application/javascript;env=frontend')
);`; );
UPDATE options SET value = 'anonymized' WHERE name IN
('documentId', 'documentSecret', 'encryptedDataKey',
'passwordVerificationHash', 'passwordVerificationSalt',
'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')
AND value != '';`;
} }
async function createAnonymizedCopy(type) { async function createAnonymizedCopy(type) {

View File

@ -4,8 +4,8 @@ const build = require('./build.js');
const packageJson = require('../../package.json'); const packageJson = require('../../package.json');
const {TRILIUM_DATA_DIR} = require('./data_dir.js'); const {TRILIUM_DATA_DIR} = require('./data_dir.js');
const APP_DB_VERSION = 227; const APP_DB_VERSION = 228;
const SYNC_VERSION = 31; const SYNC_VERSION = 32;
const CLIPPER_PROTOCOL_VERSION = "1.0"; const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = { module.exports = {

View File

@ -43,12 +43,16 @@ const optionsService = require('./options.js');
*/ */
function BackendScriptApi(currentNote, apiParams) { function BackendScriptApi(currentNote, apiParams) {
/** /**
* Note where the script started executing * Note where the script started executing (entrypoint).
* As an analogy, in C this would be the file which contains the main() function of the current process.
* @type {BNote} * @type {BNote}
*/ */
this.startNote = apiParams.startNote; this.startNote = apiParams.startNote;
/** /**
* Note where the script is currently executing. Don't mix this up with the concept of active note * Note where the script is currently executing. This comes into play when your script is spread in multiple code
* notes, the script starts in "startNote", but then through function calls may jump into another note (currentNote).
* A similar concept in C would be __FILE__
* Don't mix this up with the concept of active note.
* @type {BNote} * @type {BNote}
*/ */
this.currentNote = currentNote; this.currentNote = currentNote;
@ -288,7 +292,7 @@ function BackendScriptApi(currentNote, apiParams) {
* @param {string} params.parentNoteId * @param {string} params.parentNoteId
* @param {string} params.title * @param {string} params.title
* @param {string|Buffer} params.content * @param {string|Buffer} params.content
* @param {NoteType} params.type - text, code, file, image, search, book, relationMap, canvas * @param {NoteType} params.type - text, code, file, image, search, book, relationMap, canvas, webView
* @param {string} [params.mime] - value is derived from default mimes for type * @param {string} [params.mime] - value is derived from default mimes for type
* @param {boolean} [params.isProtected=false] * @param {boolean} [params.isProtected=false]
* @param {boolean} [params.isExpanded=false] * @param {boolean} [params.isExpanded=false]

View File

@ -5,12 +5,14 @@ const utils = require('./utils.js');
function getBlobPojo(entityName, entityId) { function getBlobPojo(entityName, entityId) {
const entity = becca.getEntity(entityName, entityId); const entity = becca.getEntity(entityName, entityId);
if (!entity) { if (!entity) {
throw new NotFoundError(`Entity ${entityName} '${entityId}' was not found.`); throw new NotFoundError(`Entity ${entityName} '${entityId}' was not found.`);
} }
const blob = becca.getBlob(entity); const blob = becca.getBlob(entity);
if (!blob) {
throw new NotFoundError(`Blob ${entity.blobId} for ${entityName} '${entityId}' was not found.`);
}
const pojo = blob.getPojo(); const pojo = blob.getPojo();

View File

@ -1 +1 @@
module.exports = { buildDate:"2023-12-07T00:03:59+01:00", buildRevision: "2e23c521c356c2305124f5df0f474532fa5f34ce" }; module.exports = { buildDate:"2024-03-03T06:58:18+01:00", buildRevision: "0ad337c8e806ba84d48d7b97aa46df52d9f236a8" };

View File

@ -48,6 +48,14 @@ function isEntityEventsDisabled() {
return !!namespace.get('disableEntityEvents'); return !!namespace.get('disableEntityEvents');
} }
function setMigrationRunning(running) {
namespace.set('migrationRunning', !!running);
}
function isMigrationRunning() {
return !!namespace.get('migrationRunning');
}
function disableSlowQueryLogging(disable) { function disableSlowQueryLogging(disable) {
namespace.set('disableSlowQueryLogging', disable); namespace.set('disableSlowQueryLogging', disable);
} }
@ -102,5 +110,7 @@ module.exports = {
putEntityChange, putEntityChange,
ignoreEntityChangeIds, ignoreEntityChangeIds,
disableSlowQueryLogging, disableSlowQueryLogging,
isSlowQueryLoggingDisabled isSlowQueryLoggingDisabled,
setMigrationRunning,
isMigrationRunning
}; };

View File

@ -17,6 +17,9 @@ const {sanitizeAttributeName} = require('./sanitize_attribute_name.js');
const noteTypes = require('../services/note_types.js').getNoteTypeNames(); const noteTypes = require('../services/note_types.js').getNoteTypeNames();
class ConsistencyChecks { class ConsistencyChecks {
/**
* @param autoFix - automatically fix all encountered problems. False is only for debugging during development (fail fast)
*/
constructor(autoFix) { constructor(autoFix) {
this.autoFix = autoFix; this.autoFix = autoFix;
this.unrecoveredConsistencyErrors = false; this.unrecoveredConsistencyErrors = false;

View File

@ -37,6 +37,8 @@ function eraseNotes(noteIdsToErase) {
function setEntityChangesAsErased(entityChanges) { function setEntityChangesAsErased(entityChanges) {
for (const ec of entityChanges) { for (const ec of entityChanges) {
ec.isErased = true; ec.isErased = true;
// we're not changing hash here, not sure if good or not
// content hash check takes isErased flag into account, though
ec.utcDateChanged = dateUtils.utcNowDateTime(); ec.utcDateChanged = dateUtils.utcNowDateTime();
entityChangesService.putEntityChangeWithForcedChange(ec); entityChangesService.putEntityChangeWithForcedChange(ec);

View File

@ -181,6 +181,8 @@ async function exportToZip(taskContext, branch, format, res, setHeaders = true)
noteIdToMeta[note.noteId] = meta; noteIdToMeta[note.noteId] = meta;
// sort children for having a stable / reproducible export format
note.sortChildren();
const childBranches = note.getChildBranches() const childBranches = note.getChildBranches()
.filter(branch => branch.noteId !== '_hidden'); .filter(branch => branch.noteId !== '_hidden');

View File

@ -219,7 +219,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
{ {
actionName: "reopenLastTab", actionName: "reopenLastTab",
defaultShortcuts: isElectron ? ["CommandOrControl+Shift+T"] : [], defaultShortcuts: isElectron ? ["CommandOrControl+Shift+T"] : [],
description: "Repoens the last closed tab", description: "Reopens the last closed tab",
scope: "window" scope: "window"
}, },
{ {
@ -291,7 +291,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
{ {
actionName: "eigthTab", actionName: "eigthTab",
defaultShortcuts: ["CommandOrControl+8"], defaultShortcuts: ["CommandOrControl+8"],
description: "Activates the eigth tab in the list", description: "Activates the eighth tab in the list",
scope: "window" scope: "window"
}, },
{ {
@ -302,7 +302,7 @@ const DEFAULT_KEYBOARD_ACTIONS = [
}, },
{ {
actionName: "lastTab", actionName: "lastTab",
defaultShortcuts: ["CommandOrControl+0"], defaultShortcuts: [],
description: "Activates the last tab in the list", description: "Activates the last tab in the list",
scope: "window" scope: "window"
}, },
@ -494,9 +494,16 @@ const DEFAULT_KEYBOARD_ACTIONS = [
separator: "Other" separator: "Other"
}, },
{
actionName: "toggleRightPane",
defaultShortcuts: [],
description: "Toggle the display of the right pane, which includes Table of Contents and Highlights",
scope: "window"
},
{ {
actionName: "printActiveNote", actionName: "printActiveNote",
defaultShortcuts: [], defaultShortcuts: [],
description: "Print active note",
scope: "window" scope: "window"
}, },
{ {

View File

@ -5,12 +5,13 @@ const log = require('./log.js');
const utils = require('./utils.js'); const utils = require('./utils.js');
const resourceDir = require('./resource_dir.js'); const resourceDir = require('./resource_dir.js');
const appInfo = require('./app_info.js'); const appInfo = require('./app_info.js');
const cls = require('./cls.js');
async function migrate() { async function migrate() {
const currentDbVersion = getDbVersion(); const currentDbVersion = getDbVersion();
if (currentDbVersion < 214) { if (currentDbVersion < 214) {
log.error("Direct migration from your current version is not supported. Please upgrade to the latest v0.60.X first and only then to this version."); log.error("Direct migration from your current version is not supported. Please upgrade to the latest v0.60.4 first and only then to this version.");
utils.crash(); utils.crash();
return; return;
@ -18,7 +19,7 @@ async function migrate() {
// backup before attempting migration // backup before attempting migration
await backupService.backupNow( await backupService.backupNow(
// creating a special backup for versions 0.60.X, the changes in 0.61 are major. // creating a special backup for version 0.60.4, the changes in 0.61 are major.
currentDbVersion === 214 currentDbVersion === 214
? `before-migration-v060` ? `before-migration-v060`
: 'before-migration' : 'before-migration'
@ -51,6 +52,9 @@ async function migrate() {
// all migrations are executed in one transaction - upgrade either succeeds, or the user can stay at the old version // all migrations are executed in one transaction - upgrade either succeeds, or the user can stay at the old version
// otherwise if half of the migrations succeed, user can't use any version - DB is too "new" for the old app, // otherwise if half of the migrations succeed, user can't use any version - DB is too "new" for the old app,
// and too old for the new app version. // and too old for the new app version.
cls.setMigrationRunning(true);
sql.transactional(() => { sql.transactional(() => {
for (const mig of migrations) { for (const mig of migrations) {
try { try {
@ -73,8 +77,11 @@ async function migrate() {
} }
}); });
log.info("VACUUMing database, this might take a while ..."); if (currentDbVersion === 214) {
sql.execute("VACUUM"); // special VACUUM after the big migration
log.info("VACUUMing database, this might take a while ...");
sql.execute("VACUUM");
}
} }
function executeMigration(mig) { function executeMigration(mig) {

View File

@ -471,6 +471,8 @@ function findRelationMapLinks(content, foundLinks) {
const imageUrlToAttachmentIdMapping = {}; const imageUrlToAttachmentIdMapping = {};
async function downloadImage(noteId, imageUrl) { async function downloadImage(noteId, imageUrl) {
const unescapedUrl = utils.unescapeHtml(imageUrl);
try { try {
let imageBuffer; let imageBuffer;
@ -487,14 +489,14 @@ async function downloadImage(noteId, imageUrl) {
}); });
}); });
} else { } else {
imageBuffer = await request.getImage(imageUrl); imageBuffer = await request.getImage(unescapedUrl);
} }
const parsedUrl = url.parse(imageUrl); const parsedUrl = url.parse(unescapedUrl);
const title = path.basename(parsedUrl.pathname); const title = path.basename(parsedUrl.pathname);
const imageService = require('../services/image.js'); const imageService = require('../services/image.js');
const {attachment} = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true); const attachment = imageService.saveImageToAttachment(noteId, imageBuffer, title, true, true);
imageUrlToAttachmentIdMapping[imageUrl] = attachment.attachmentId; imageUrlToAttachmentIdMapping[imageUrl] = attachment.attachmentId;
@ -511,7 +513,7 @@ const downloadImagePromises = {};
function replaceUrl(content, url, attachment) { function replaceUrl(content, url, attachment) {
const quotedUrl = utils.quoteRegex(url); const quotedUrl = utils.quoteRegex(url);
return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "ig"), ` src="api/attachments/${encodeURIComponent(attachment.title)}/image"`); return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "ig"), ` src="api/attachments/${attachment.attachmentId}/image/${encodeURIComponent(attachment.title)}"`);
} }
function downloadImages(noteId, content) { function downloadImages(noteId, content) {
@ -636,6 +638,10 @@ function saveAttachments(note, content) {
content = `${content.substr(0, attachmentMatch.index)}<a class="reference-link" href="#root/${note.noteId}?viewMode=attachments&attachmentId=${attachment.attachmentId}">${title}</a>${content.substr(attachmentMatch.index + attachmentMatch[0].length)}`; content = `${content.substr(0, attachmentMatch.index)}<a class="reference-link" href="#root/${note.noteId}?viewMode=attachments&attachmentId=${attachment.attachmentId}">${title}</a>${content.substr(attachmentMatch.index + attachmentMatch[0].length)}`;
} }
// removing absolute references to server to keep it working between instances,
// we also omit / at the beginning to keep the paths relative
content = content.replace(/src="[^"]*\/api\/attachments\//g, 'src="api/attachments/');
return content; return content;
} }
@ -889,6 +895,11 @@ function scanForLinks(note, content) {
* Things which have to be executed after updating content, but asynchronously (separate transaction) * Things which have to be executed after updating content, but asynchronously (separate transaction)
*/ */
async function asyncPostProcessContent(note, content) { async function asyncPostProcessContent(note, content) {
if (cls.isMigrationRunning()) {
// this is rarely needed for migrations, but can cause trouble by e.g. triggering downloads
return;
}
if (note.hasStringContent() && !utils.isString(content)) { if (note.hasStringContent() && !utils.isString(content)) {
content = content.toString(); content = content.toString();
} }

View File

@ -63,10 +63,15 @@ function exec(opts) {
} }
let responseStr = ''; let responseStr = '';
let chunks = [];
response.on('data', chunk => responseStr += chunk); response.on('data', chunk => chunks.push(chunk));
response.on('end', () => { response.on('end', () => {
// use Buffer instead of string concatenation to avoid implicit decoding for each chunk
// decode the entire data chunks explicitly as utf-8
responseStr = Buffer.concat(chunks).toString('utf-8')
if ([200, 201, 204].includes(response.statusCode)) { if ([200, 201, 204].includes(response.statusCode)) {
try { try {
const jsonObj = responseStr.trim() ? JSON.parse(responseStr) : null; const jsonObj = responseStr.trim() ? JSON.parse(responseStr) : null;

View File

@ -111,11 +111,7 @@ class NoteContentFulltextExp extends Expression {
if (type === 'text' && mime === 'text/html') { if (type === 'text' && mime === 'text/html') {
if (!this.raw && content.length < 20000) { // striptags is slow for very large notes if (!this.raw && content.length < 20000) { // striptags is slow for very large notes
// allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412 content = this.stripTags(content);
content = striptags(content, ['a'], ' ');
// at least the closing tag can be easily stripped
content = content.replace(/<\/a>/ig, "");
} }
content = content.replace(/&nbsp;/g, ' '); content = content.replace(/&nbsp;/g, ' ');
@ -123,6 +119,23 @@ class NoteContentFulltextExp extends Expression {
return content.trim(); return content.trim();
} }
stripTags(content) {
// we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
// we want to insert space in place of block tags (because they imply text separation)
// but we don't want to insert text for typical formatting inline tags which can occur within one word
const linkTag = 'a';
const inlineFormattingTags = ['b', 'strong', 'em', 'i', 'span', 'big', 'small', 'font', 'sub', 'sup'];
// replace tags which imply text separation with a space
content = striptags(content, [linkTag, ...inlineFormattingTags], ' ');
// replace the inline formatting tags (but not links) without a space
content = striptags(content, [linkTag], '');
// at least the closing link tag can be easily stripped
return content.replace(/<\/a>/ig, "");
}
} }
module.exports = NoteContentFulltextExp; module.exports = NoteContentFulltextExp;

View File

@ -73,7 +73,6 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext
if (localEC?.isErased) { if (localEC?.isErased) {
eraseEntity(remoteEC); // make sure it's erased anyway eraseEntity(remoteEC); // make sure it's erased anyway
updateContext.alreadyErased++; updateContext.alreadyErased++;
return false; // we won't save entitychange in this case
} else { } else {
eraseEntity(remoteEC); eraseEntity(remoteEC);
updateContext.erased++; updateContext.erased++;
@ -91,12 +90,17 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext
updateContext.updated[remoteEC.entityName].push(remoteEC.entityId); updateContext.updated[remoteEC.entityName].push(remoteEC.entityId);
} }
if (!localEC || localEC.utcDateChanged < remoteEC.utcDateChanged || localEC.hash !== remoteEC.hash) { if (!localEC
|| localEC.utcDateChanged < remoteEC.utcDateChanged
|| localEC.hash !== remoteEC.hash
|| localEC.isErased !== remoteEC.isErased
) {
entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId); entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId);
} }
return true; return true;
} else if (localEC.hash !== remoteEC.hash && localEC.utcDateChanged > remoteEC.utcDateChanged) { } else if ((localEC.hash !== remoteEC.hash || localEC.isErased !== remoteEC.isErased)
&& localEC.utcDateChanged > remoteEC.utcDateChanged) {
// the change on our side is newer than on the other side, so the other side should update // the change on our side is newer than on the other side, so the other side should update
entityChangesService.putEntityChangeForOtherInstances(localEC); entityChangesService.putEntityChangeForOtherInstances(localEC);
@ -148,7 +152,7 @@ function eraseEntity(entityChange) {
]; ];
if (!entityNames.includes(entityName)) { if (!entityNames.includes(entityName)) {
log.error(`Cannot erase entity '${entityName}', id '${entityId}'.`); log.error(`Cannot erase ${entityName} '${entityId}'.`);
return; return;
} }

View File

@ -34,8 +34,8 @@ function hashedBlobId(content) {
// we don't want such + and / in the IDs // we don't want such + and / in the IDs
const kindaBase62Hash = base64Hash const kindaBase62Hash = base64Hash
.replace('+', 'X') .replaceAll('+', 'X')
.replace('/', 'Y'); .replaceAll('/', 'Y');
// 20 characters of base62 gives us ~120 bit of entropy which is plenty enough // 20 characters of base62 gives us ~120 bit of entropy which is plenty enough
return kindaBase62Hash.substr(0, 20); return kindaBase62Hash.substr(0, 20);

View File

@ -105,10 +105,10 @@ function renderText(result, note) {
if (result.content.includes(`<span class="math-tex">`)) { if (result.content.includes(`<span class="math-tex">`)) {
result.header += ` result.header += `
<script src="../../${assetPath}/libraries/katex/katex.min.js"></script> <script src="../../${assetPath}/node_modules/katex/dist/katex.min.js"></script>
<link rel="stylesheet" href="../../${assetPath}/libraries/katex/katex.min.css"> <link rel="stylesheet" href="../../${assetPath}/node_modules/katex/dist/katex.min.css">
<script src="../../${assetPath}/libraries/katex/auto-render.min.js"></script> <script src="../../${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
<script src="../../${assetPath}/libraries/katex/mhchem.min.js"></script> <script src="../../${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.getElementById('content')); renderMathInElement(document.getElementById('content'));
@ -137,7 +137,7 @@ function renderCode(result) {
function renderMermaid(result, note) { function renderMermaid(result, note) {
result.content = ` result.content = `
<img src="api/images/${note.noteId}/${note.escapedTitle}?${note.utcDateModified}"> <img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">
<hr> <hr>
<details> <details>
<summary>Chart source</summary> <summary>Chart source</summary>
@ -146,7 +146,7 @@ function renderMermaid(result, note) {
} }
function renderImage(result, note) { function renderImage(result, note) {
result.content = `<img src="api/images/${note.noteId}/${note.escapedTitle}?${note.utcDateModified}">`; result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">`;
} }
function renderFile(note, result) { function renderFile(note, result) {

View File

@ -490,6 +490,10 @@ class SNote extends AbstractShacaEntity {
return escape(this.title); return escape(this.title);
} }
get encodedTitle() {
return encodeURIComponent(this.title);
}
getPojo() { getPojo() {
return { return {
noteId: this.noteId, noteId: this.noteId,

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
<link rel="manifest" href="manifest.webmanifest"> <link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>Trilium Notes</title> <title>Trilium Notes</title>
</head> </head>
<body class="desktop heading-style-<%= headingStyle %>"> <body class="desktop heading-style-<%= headingStyle %>">

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="theme-color" content="#fff"> <meta name="theme-color" content="#fff">
<title>Trilium Notes</title> <title>Trilium Notes</title>
<link rel="manifest" href="manifest.webmanifest"> <link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<style> <style>
.lds-roller { .lds-roller {

View File

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
[[ ! -z "${USER_UID}" ]] && usermod -u ${USER_UID} node || echo "No USER_UID specified, leaving 1000" [[ ! -z "${USER_UID}" ]] && usermod -u ${USER_UID} node || echo "No USER_UID specified, leaving 1000"
[[ ! -z "${USER_GID}" ]] && groupmod -g ${USER_GID} node || echo "No USER_GID specified, leaving 1000" [[ ! -z "${USER_GID}" ]] && groupmod -og ${USER_GID} node || echo "No USER_GID specified, leaving 1000"
chown -R node:node /home/node chown -R node:node /home/node
exec su-exec node node ./src/www exec su-exec node node ./src/www