diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 9e586b92e..000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Ignore "third party" code whose style we will not change.
-/boot/sjcl.js
-/core/modules/utils/base64-utf8/base64-utf8.module.js
-/core/modules/utils/base64-utf8/base64-utf8.module.min.js
-/core/modules/utils/diff-match-patch/diff_match_patch.js
-/core/modules/utils/diff-match-patch/diff_match_patch_uncompressed.js
-/core/modules/utils/dom/csscolorparser.js
-/plugins/tiddlywiki/*/files/
diff --git a/.eslintrc.yml b/.eslintrc.yml
deleted file mode 100644
index 049af59e4..000000000
--- a/.eslintrc.yml
+++ /dev/null
@@ -1,283 +0,0 @@
-env:
- browser: true
- commonjs: true
- es2021: true
- node: true
-extends: 'eslint:recommended'
-globals:
- "$tw": "writable" # temporary
-parserOptions:
- ecmaVersion: 5
-rules:
- array-bracket-newline: 'off'
- array-bracket-spacing: 'off'
- array-callback-return: 'off'
- array-element-newline: 'off'
- arrow-body-style: error
- arrow-parens:
- - error
- - as-needed
- arrow-spacing:
- - error
- - after: true
- before: true
- block-scoped-var: 'off'
- block-spacing: 'off'
- brace-style: 'off'
- callback-return: 'off'
- camelcase: 'off'
- capitalized-comments: 'off'
- class-methods-use-this: error
- comma-dangle: 'off'
- comma-spacing: 'off'
- comma-style: 'off'
- complexity: 'off'
- computed-property-spacing: 'off'
- consistent-return: 'off'
- consistent-this: 'off'
- curly: 'off'
- default-case: 'off'
- default-case-last: error
- default-param-last: error
- dot-location: 'off'
- dot-notation: 'off'
- eol-last: 'off'
- eqeqeq: 'off'
- func-call-spacing: 'off'
- func-name-matching: 'off'
- func-names: 'off'
- func-style: 'off'
- function-call-argument-newline: 'off'
- function-paren-newline: 'off'
- generator-star-spacing: error
- global-require: 'off'
- grouped-accessor-pairs: error
- guard-for-in: 'off'
- handle-callback-err: 'off'
- id-blacklist: error
- id-denylist: error
- id-length: 'off'
- id-match: error
- implicit-arrow-linebreak: error
- indent: 'off'
- indent-legacy: 'off'
- init-declarations: 'off'
- jsx-quotes: error
- key-spacing: 'off'
- keyword-spacing:
- - error
- - before: true
- after: false
- overrides:
- 'case':
- after: true
- 'do':
- 'after': true
- 'else':
- after: true
- 'return':
- after: true
- 'throw':
- after: true
- 'try':
- after: true
- line-comment-position: 'off'
- linebreak-style: 'off'
- lines-around-comment: 'off'
- lines-around-directive: 'off'
- lines-between-class-members: error
- max-classes-per-file: error
- max-depth: 'off'
- max-len: 'off'
- max-lines: 'off'
- max-lines-per-function: 'off'
- max-nested-callbacks: error
- max-params: 'off'
- max-statements: 'off'
- max-statements-per-line: 'off'
- multiline-comment-style: 'off'
- multiline-ternary: 'off'
- new-parens: 'off'
- newline-after-var: 'off'
- newline-before-return: 'off'
- newline-per-chained-call: 'off'
- no-alert: 'off'
- no-array-constructor: 'off'
- no-await-in-loop: error
- no-bitwise: 'off'
- no-buffer-constructor: 'off'
- no-caller: error
- no-catch-shadow: 'off'
- no-confusing-arrow: error
- no-console: 'off'
- no-constant-condition:
- - error
- - checkLoops: false
- no-constructor-return: error
- no-continue: 'off'
- no-div-regex: 'off'
- no-duplicate-imports: error
- no-else-return: 'off'
- no-empty-function: 'off'
- no-eq-null: 'off'
- no-eval: 'off'
- no-extend-native: 'off'
- no-extra-bind: 'off'
- no-extra-label: 'off'
- no-extra-parens: 'off'
- no-floating-decimal: 'off'
- no-implicit-coercion:
- - error
- - boolean: false
- number: false
- string: false
- no-implicit-globals: 'off'
- no-implied-eval: error
- no-inline-comments: 'off'
- no-invalid-this: 'off'
- no-iterator: error
- no-label-var: 'off'
- no-labels: 'off'
- no-lone-blocks: 'off'
- no-lonely-if: 'off'
- no-loop-func: 'off'
- no-loss-of-precision: error
- no-magic-numbers: 'off'
- no-mixed-operators: 'off'
- no-mixed-requires: 'off'
- no-multi-assign: 'off'
- no-multi-spaces: 'off'
- no-multi-str: error
- no-multiple-empty-lines: 'off'
- no-native-reassign: 'off'
- no-negated-condition: 'off'
- no-negated-in-lhs: error
- no-nested-ternary: 'off'
- no-new: 'off'
- no-new-func: 'off'
- no-new-object: 'off'
- no-new-require: error
- no-new-wrappers: error
- no-octal-escape: error
- no-param-reassign: 'off'
- no-path-concat: error
- no-plusplus: 'off'
- no-process-env: 'off'
- no-process-exit: 'off'
- no-promise-executor-return: error
- no-proto: 'off'
- no-restricted-exports: error
- no-restricted-globals: error
- no-restricted-imports: error
- no-restricted-modules: error
- no-restricted-properties: error
- no-restricted-syntax: error
- no-return-assign: 'off'
- no-return-await: error
- no-script-url: 'off'
- no-self-compare: 'off'
- no-sequences: 'off'
- no-shadow: 'off'
- no-spaced-func: 'off'
- no-sync: 'off'
- no-tabs: 'off'
- no-template-curly-in-string: error
- no-ternary: 'off'
- no-throw-literal: 'off'
- no-trailing-spaces: 'off'
- no-undef-init: 'off'
- no-undefined: 'off'
- no-underscore-dangle: 'off'
- no-unmodified-loop-condition: 'off'
- no-unneeded-ternary: 'off'
- no-unreachable-loop: error
- no-unused-expressions: 'off'
- no-use-before-define: 'off'
- no-useless-backreference: error
- no-useless-call: 'off'
- no-useless-computed-key: error
- no-useless-concat: 'off'
- no-useless-constructor: error
- no-useless-rename: error
- no-useless-return: 'off'
- no-var: 'off'
- no-void: 'off'
- no-warning-comments: 'off'
- no-whitespace-before-property: error
- nonblock-statement-body-position:
- - error
- - any
- object-curly-newline: 'off'
- object-curly-spacing: 'off'
- object-property-newline: 'off'
- object-shorthand: 'off'
- one-var: 'off'
- one-var-declaration-per-line: 'off'
- operator-assignment: 'off'
- operator-linebreak: 'off'
- padded-blocks: 'off'
- padding-line-between-statements: error
- prefer-arrow-callback: 'off'
- prefer-const: 'off'
- prefer-destructuring: 'off'
- prefer-exponentiation-operator: 'off'
- prefer-named-capture-group: 'off'
- prefer-numeric-literals: error
- prefer-object-spread: 'off'
- prefer-promise-reject-errors: error
- prefer-reflect: 'off'
- prefer-regex-literals: 'off'
- prefer-rest-params: 'off'
- prefer-spread: 'off'
- prefer-template: 'off'
- quote-props: 'off'
- quotes: 'off'
- radix: 'off'
- require-atomic-updates: error
- require-await: error
- require-jsdoc: 'off'
- require-unicode-regexp: 'off'
- rest-spread-spacing: error
- semi: 'off'
- semi-spacing: 'off'
- semi-style: 'off'
- sort-imports: error
- sort-keys: 'off'
- sort-vars: 'off'
- space-before-blocks: 'off'
- space-before-function-paren: 'off'
- space-in-parens: 'off'
- space-infix-ops: 'off'
- space-unary-ops: 'off'
- spaced-comment: 'off'
- strict: 'off'
- switch-colon-spacing: 'off'
- symbol-description: error
- template-curly-spacing: error
- template-tag-spacing: error
- unicode-bom:
- - error
- - never
- valid-jsdoc: 'off'
- valid-typeof:
- - error
- - requireStringLiterals: false
- vars-on-top: 'off'
- wrap-iife: 'off'
- wrap-regex: 'off'
- yield-star-spacing: error
- yoda: 'off'
-
- # temporary rules
- no-useless-escape: 'off'
- no-unused-vars: 'off'
- no-empty: 'off'
- no-extra-semi: 'off'
- no-redeclare: 'off'
- no-control-regex: "off"
- no-mixed-spaces-and-tabs: "off"
- no-extra-boolean-cast: "off"
- no-prototype-builtins: "off"
- no-undef: "off"
- no-unreachable: "off"
- no-self-assign: "off"
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..d5bce4e9b
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+boot/** -linguist-generated
+**/tiddlywiki.files linguist-language=JSON
+**/tiddlywiki.info linguist-language=JSON
+**/plugin.info linguist-language=JSON
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..a07ea530a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,62 @@
+---
+name: Bug report
+about: Create a report to help us improve TiddlyWiki 5
+title: "[Report] "
+type: report
+
+---
+
+
+
+
+**Problem Description**
+
+
+
+**To Reproduce**
+
+Steps to reproduce the behavior:
+
+1. At https://tiddlywiki.com
+2. Click on ...
+3. Scroll down to ...
+4. See ...
+
+
+**Expected behavior**
+
+As a user,
+
+I would expect ...
+
+
+**TiddlyWiki Configuration**
+
+
+- Report created with: [Wiki Information](https://tiddlywiki.com/#%24%3A%2Fcore%2Fui%2FControlPanel%2FWikiInformation)
+
+
+
+
+
+- Version:
+- Saving mechanism:
+- Plugins installed:
+
+
+**Desktop**
+
+
+- OS:
+- Browser:
+
+**Smartphone**
+
+
+- Device:
+- OS:
+- Browser:
+
+
+**Additional context**
+
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
deleted file mode 100644
index 1e644e161..000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ /dev/null
@@ -1,67 +0,0 @@
-name: Bug report
-description: Create a report to help us improve TiddlyWiki 5
-title: "[BUG] "
-body:
- - type: textarea
- id: Describe
- attributes:
- label: Describe the bug
- description: A clear and concise description of what the bug is.
- validations:
- required: true
- - type: textarea
- id: Expected
- attributes:
- label: Expected behavior
- description: A clear and concise description of what you expected to happen.
- validations:
- required: false
- - type: textarea
- id: Reproduce
- attributes:
- label: To Reproduce
- description: "Steps to reproduce the behavior:"
- value: |
- 1. Go to '...'
- 2. Click on '....'
- 3. Scroll down to '....'
- 4. See error
- validations:
- required: false
- - type: textarea
- id: Screenshots
- attributes:
- label: Screenshots
- description: If applicable, add screenshots to help explain your problem.
- placeholder: Drag image here to upload screenshot!
- validations:
- required: false
- - type: textarea
- id: Configuration
- attributes:
- label: TiddlyWiki Configuration
- description: please complete the following information
- value: |
- - Version [e.g. v5.1.24]
- - Saving mechanism [e.g. Node.js, TiddlyDesktop, TiddlyHost etc]
- - Plugins installed [e.g. Freelinks, TiddlyMap]
-
- ### Desktop (please complete the following information):
-
- - OS: [e.g. iOS]
- - Browser [e.g. chrome, safari]
- - Version [e.g. 22]
-
- ### Smartphone (please complete the following information):
-
- - Device: [e.g. iPhone6]
- - OS: [e.g. iOS8.1]
- - Browser [e.g. stock browser, safari]
- - Version [e.g. 22]
- validations:
- required: true
- - type: textarea
- id: Context
- attributes:
- label: Additional context
- description: Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 556b93919..dca23b783 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Discuss feature request
- url: https://github.com/Jermolene/TiddlyWiki5/discussions
+ url: https://github.com/TiddlyWiki/TiddlyWiki5/discussions
about: Open new discussion about new feature
- name: Talk.Tiddlywiki Forum
url: https://talk.tiddlywiki.org
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 6eb55dd6b..c6a7f8022 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -4,17 +4,23 @@ about: Suggest an idea for TiddlyWiki 5
title: "[IDEA]"
labels: ''
assignees: ''
+type: idea
---
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+**Is your idea related to a problem? Please describe.**
+
+A clear and concise description of what the problem is. Eg:
+As a user, I would like [...]
**Describe the solution you'd like**
+
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
+
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
+
Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8daf2f468..f6fb58f7d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,16 +5,22 @@ on:
- master
- tiddlywiki-com
env:
- NODE_VERSION: "12"
+ NODE_VERSION: "22"
jobs:
test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v5
+ - uses: actions/setup-node@v4
with:
node-version: "${{ env.NODE_VERSION }}"
- - run: "./bin/test.sh"
+ - run: "./bin/ci-test.sh"
+ - uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 30
build-prerelease:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
@@ -24,8 +30,8 @@ jobs:
TW5_BUILD_MAIN_EDITION: "./editions/prerelease"
TW5_BUILD_OUTPUT: "./output/prerelease"
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v5
+ - uses: actions/setup-node@v4
with:
node-version: "${{ env.NODE_VERSION }}"
- run: "./bin/ci-pre-build.sh"
@@ -54,9 +60,10 @@ jobs:
TW5_BUILD_TIDDLYWIKI: "./node_modules/tiddlywiki/tiddlywiki.js"
TW5_BUILD_MAIN_EDITION: "./editions/tw5.com"
TW5_BUILD_OUTPUT: "./output"
+ TW5_BUILD_ARCHIVE: "./output"
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: actions/checkout@v5
+ - uses: actions/setup-node@v4
with:
node-version: "${{ env.NODE_VERSION }}"
- run: "./bin/ci-pre-build.sh"
diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml
new file mode 100644
index 000000000..331727b71
--- /dev/null
+++ b/.github/workflows/cla-check.yml
@@ -0,0 +1,30 @@
+name: Check CLA Signature
+on:
+ pull_request_target:
+ types:
+ - opened
+ - reopened
+ paths-ignore:
+ - 'licenses/cla-individual.md'
+jobs:
+ check_cla:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ if: ${{ (github.event.pull_request.user.login != github.repository_owner) }}
+ steps:
+ - run: |
+ if ! curl -s https://raw.githubusercontent.com/Jermolene/TiddlyWiki5/tiddlywiki-com/licenses/cla-individual.md | grep -io "@$USER,"; then
+ echo "CLA not signed"
+ gh pr comment "$NUMBER" -b "@$USER It appears that this is your first contribution to the project, welcome.
+
+ With apologies for the bureaucracy, please could you prepare a separate PR to the 'tiddlywiki-com' branch with your signature for the Contributor License Agreement (see [contributing.md](https://github.com/TiddlyWiki/TiddlyWiki5/blob/master/contributing.md))."
+ else
+ echo "CLA already signed"
+ gh pr comment "$NUMBER" -b "Confirmed: **$USER** has already signed the Contributor License Agreement (see [contributing.md](https://github.com/TiddlyWiki/TiddlyWiki5/blob/master/contributing.md))"
+ fi
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_REPO: ${{ github.repository }}
+ NUMBER: ${{ github.event.pull_request.number }}
+ USER: ${{ github.actor }}
diff --git a/.github/workflows/cla-signed.yml b/.github/workflows/cla-signed.yml
new file mode 100644
index 000000000..01d57d014
--- /dev/null
+++ b/.github/workflows/cla-signed.yml
@@ -0,0 +1,70 @@
+name: CLA Signed
+
+on:
+ pull_request_target:
+ types:
+ - opened
+ - closed
+ paths:
+ - 'licenses/cla-individual.md'
+
+env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_REPO: ${{ github.repository }}
+ NUMBER: ${{ github.event.pull_request.number }}
+ AUTHOR: ${{ github.event.pull_request.user.login }}
+
+jobs:
+ # check if PRs updating the CLA are targetting the tiddlywiki-com branch
+ check-signature-branch:
+ if: (github.event.pull_request.merged != true) && (github.event.pull_request.user.login != github.repository_owner)
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - run: |
+ if [[ "$BRANCH" != "tiddlywiki-com" ]]; then
+ echo "This CLA signature targets the wrong branch: $BRANCH"
+ gh pr comment "$NUMBER" -b "@$AUTHOR Signatures to the CLA must target the 'tiddlywiki-com' branch."
+ fi
+ env:
+ BRANCH: ${{ github.event.pull_request.base.ref }}
+
+ # leave a comment on each open PR by a given author when their signature is added to the CLA
+ cla-signed:
+ if: (github.event.pull_request.merged == true) && (github.event.pull_request.user.login != github.repository_owner)
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ steps:
+ - name: List open PRs by user
+ id: list-prs
+ uses: actions/github-script@v6
+ with:
+ result-encoding: string
+ script: |
+ const owner = context.repo.owner,
+ repo = context.repo.repo,
+ author = context.payload.pull_request.user.login;
+
+ const { data: pullRequests } = await github.rest.pulls.list({
+ owner: owner,
+ repo: repo,
+ state: 'open',
+ sort: 'created',
+ direction: 'desc',
+ per_page: 100
+ });
+ const userPullRequests = pullRequests.filter(pr => pr.user.login === author),
+ prNumbers = userPullRequests.map(pr => pr.number).join(',');
+ console.log(`Open pull requests by ${author}:${prNumbers}`);
+ return prNumbers;
+
+ - name: Comment open PRs by the same author
+ run: |
+ prs=($(echo ${{ steps.list-prs.outputs.result }} | tr "," "\n"))
+
+ for number in "${prs[@]}"
+ do
+ gh pr comment "$number" -b "**$AUTHOR** has signed the Contributor License Agreement (see [contributing.md](https://github.com/TiddlyWiki/TiddlyWiki5/blob/master/contributing.md))"
+ done
diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml
new file mode 100644
index 000000000..eae1d2c46
--- /dev/null
+++ b/.github/workflows/eslint.yml
@@ -0,0 +1,40 @@
+name: ESLint
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+ workflow_dispatch:
+
+concurrency:
+ group: lint-${{ github.event.pull_request.number || github.ref_name }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ # Needed for GitHub Checks API
+ checks: write
+
+jobs:
+ eslint:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: Install dependencies
+ run: npm install --include=dev
+
+ - name: Run ESLint with reviewdog (GitHub Checks)
+ uses: reviewdog/action-eslint@v1
+ with:
+ eslint_flags: '.'
+ reporter: github-pr-check
+ fail_level: error
+ level: error
+ tool_name: ESLint PR code
diff --git a/.github/workflows/pr-check-build-size.yml b/.github/workflows/pr-check-build-size.yml
new file mode 100644
index 000000000..de05b0c4f
--- /dev/null
+++ b/.github/workflows/pr-check-build-size.yml
@@ -0,0 +1,55 @@
+name: Calculate PR build size
+on:
+ pull_request_target:
+ types: [opened, reopened, synchronize]
+ paths:
+ - 'boot/**'
+ - 'core/**'
+ - 'themes/tiddlywiki/snowwhite/**'
+ - 'themes/tiddlywiki/vanilla/**'
+
+jobs:
+ calculate-build-size:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: read
+ contents: read
+ outputs:
+ pr_size: ${{ steps.get_sizes.outputs.pr_size }}
+ base_size: ${{ steps.get_sizes.outputs.base_size }}
+ steps:
+ - name: build-size-check
+ id: get_sizes
+ uses: TiddlyWiki/cerebrus@v7
+ with:
+ pr_number: ${{ github.event.pull_request.number }}
+ repo: ${{ github.repository }}
+ base_ref: ${{ github.event.pull_request.base.ref }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ mode: size:calc
+
+ dispatch-followup:
+ needs: calculate-build-size
+ runs-on: ubuntu-latest
+ permissions:
+ actions: write # Required to dispatch another workflow
+ pull-requests: write
+ contents: read
+ steps:
+ - name: Trigger follow-up workflow
+ uses: actions/github-script@v6
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ await github.rest.actions.createWorkflowDispatch({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ workflow_id: 'pr-comment-build-size.yml',
+ ref: 'master',
+ inputs: {
+ pr_number: '${{ github.event.pull_request.number }}',
+ base_ref: '${{ github.event.pull_request.base.ref }}',
+ pr_size: '${{ needs.calculate-build-size.outputs.pr_size }}',
+ base_size: '${{ needs.calculate-build-size.outputs.base_size }}'
+ }
+ });
\ No newline at end of file
diff --git a/.github/workflows/pr-comment-build-size.yml b/.github/workflows/pr-comment-build-size.yml
new file mode 100644
index 000000000..60c81d00a
--- /dev/null
+++ b/.github/workflows/pr-comment-build-size.yml
@@ -0,0 +1,36 @@
+name: Comment on PR build size (Trusted workflow)
+
+on:
+ workflow_dispatch:
+ inputs:
+ pr_number:
+ required: true
+ type: string
+ base_ref:
+ required: true
+ type: string
+ pr_size:
+ required: true
+ type: string
+ base_size:
+ required: true
+ type: string
+
+jobs:
+ comment-on-pr:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ contents: read
+
+ steps:
+ - name: Build and check size
+ uses: TiddlyWiki/cerebrus@v7
+ with:
+ pr_number: ${{ inputs.pr_number }}
+ repo: ${{ github.repository }}
+ base_ref: ${{ inputs.base_ref }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ mode: size:comment
+ pr_size: ${{ inputs.pr_size }}
+ base_size: ${{ inputs.base_size }}
diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
new file mode 100644
index 000000000..e7e3bd973
--- /dev/null
+++ b/.github/workflows/pr-validation.yml
@@ -0,0 +1,37 @@
+name: PR Validation
+
+on:
+ pull_request_target:
+ types: [opened, reopened, synchronize]
+
+permissions:
+ contents: read
+ pull-requests: write
+ issues: write
+jobs:
+ validate-pr:
+ runs-on: ubuntu-latest
+
+ steps:
+ # Step 1: Validate PR paths
+ - name: Validate PR Paths
+ uses: TiddlyWiki/cerebrus@v6
+ with:
+ pr_number: ${{ github.event.pull_request.number }}
+ repo: ${{ github.repository }}
+ base_ref: ${{ github.event.pull_request.base.ref }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ mode: rules
+ continue-on-error: true
+
+ # Step 2: Validate change notes
+ - name: Validate Change Notes
+ uses: TiddlyWiki/cerebrus@v7
+ with:
+ pr_number: ${{ github.event.pull_request.number }}
+ repo: ${{ github.repository }}
+ base_ref: ${{ github.event.pull_request.base.ref }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ mode: changenotes
+ continue-on-error: false
+
diff --git a/.gitignore b/.gitignore
index ad7e8e07f..412759161 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,11 @@
.DS_Store
.c9/
+.vs/
.vscode/
tmp/
output/
node_modules/
-
+/test-results/
+/playwright-report/
+/playwright/.cache/
+$__StoryList.tid
diff --git a/bin/build-site.sh b/bin/build-site.sh
index 7f56e6d30..4ad1c1bcd 100755
--- a/bin/build-site.sh
+++ b/bin/build-site.sh
@@ -5,7 +5,7 @@
# Default to the current version number for building the plugin library
if [ -z "$TW5_BUILD_VERSION" ]; then
- TW5_BUILD_VERSION=v5.2.6
+ TW5_BUILD_VERSION=v5.4.0
fi
echo "Using TW5_BUILD_VERSION as [$TW5_BUILD_VERSION]"
@@ -73,10 +73,8 @@ rm $TW5_BUILD_OUTPUT/dev/static/*
echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/tw2parser/index.html" > $TW5_BUILD_OUTPUT/classicparserdemo.html
echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/codemirror/index.html" > $TW5_BUILD_OUTPUT/codemirrordemo.html
-echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/d3/index.html" > $TW5_BUILD_OUTPUT/d3demo.html
echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/highlight/index.html" > $TW5_BUILD_OUTPUT/highlightdemo.html
echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/markdown/index.html" > $TW5_BUILD_OUTPUT/markdowndemo.html
-echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/tahoelafs/index.html" > $TW5_BUILD_OUTPUT/tahoelafs.html
# Put the build details into a .tid file so that it can be included in each build (deleted at the end of this script)
@@ -84,40 +82,56 @@ echo -e -n "title: $:/build\ncommit: $TW5_BUILD_COMMIT\n\n$TW5_BUILD_DETAILS\n"
######################################################
#
-# Core distribution
+# Core distributions
#
######################################################
+# Conditionally build archive if $TW5_BUILD_ARCHIVE variable is set, otherwise do nothing
+#
+# /archive/Empty-TiddlyWiki-.html Empty archived version
+# /archive/TiddlyWiki-.html Full archived version
+
+if [ -n "$TW5_BUILD_ARCHIVE" ]; then
+
+node $TW5_BUILD_TIDDLYWIKI \
+ $TW5_BUILD_MAIN_EDITION \
+ --version \
+ --load $TW5_BUILD_OUTPUT/build.tid \
+ --output $TW5_BUILD_ARCHIVE \
+ --build archive \
+ || exit 1
+fi
+
# /index.html Main site
+# /external-(version).html External core version of main site
# /favicon.ico Favicon for main site
# /static.html Static rendering of default tiddlers
# /alltiddlers.html Static rendering of all tiddlers
# /static/* Static single tiddlers
# /static/static.css Static stylesheet
# /static/favicon.ico Favicon for static pages
+
node $TW5_BUILD_TIDDLYWIKI \
$TW5_BUILD_MAIN_EDITION \
- --verbose \
--version \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
- --build favicon static index \
+ --build favicon static index external-js \
|| exit 1
-# /empty.html Empty
-# /empty.hta For Internet Explorer
+# /empty.html Empty
+# /empty-external-core.html External core empty
+# /tiddlywikicore-.js Core plugin javascript
node $TW5_BUILD_TIDDLYWIKI \
- $TW5_BUILD_MAIN_EDITION \
- --verbose \
+ ./editions/empty \
--output $TW5_BUILD_OUTPUT \
- --build empty \
+ --build empty emptyexternalcore \
|| exit 1
# /test.html Test edition
node $TW5_BUILD_TIDDLYWIKI \
./editions/test \
- --verbose \
--output $TW5_BUILD_OUTPUT \
--rendertiddler $:/core/save/all test.html text/plain \
|| exit 1
@@ -130,16 +144,28 @@ node $TW5_BUILD_TIDDLYWIKI \
# /dev/static/static.css Static stylesheet
node $TW5_BUILD_TIDDLYWIKI \
./editions/dev \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/dev \
--build index favicon static \
|| exit 1
+# /tour.html tour edition
+node $TW5_BUILD_TIDDLYWIKI \
+ ./editions/tour \
+ --output $TW5_BUILD_OUTPUT \
+ --rendertiddler $:/core/save/all-external-js tour.html text/plain \
+ || exit 1
+
+# /surveys.html surveys edition
+node $TW5_BUILD_TIDDLYWIKI \
+ ./editions/tiddlywiki-surveys \
+ --output $TW5_BUILD_OUTPUT \
+ --build index \
+ || exit 1
+
# /share.html Custom edition for sharing via the URL
node $TW5_BUILD_TIDDLYWIKI \
./editions/share \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
--build share \
@@ -148,7 +174,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /upgrade.html Custom edition for performing upgrades
node $TW5_BUILD_TIDDLYWIKI \
./editions/upgrade \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
--build upgrade \
@@ -157,7 +182,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /encrypted.html Copy of the main file encrypted with the password "password"
node $TW5_BUILD_TIDDLYWIKI \
$TW5_BUILD_MAIN_EDITION \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
--build encrypted \
@@ -173,16 +197,14 @@ node $TW5_BUILD_TIDDLYWIKI \
# /editions/xlsx-utils/index.html xlsx-utils edition
node $TW5_BUILD_TIDDLYWIKI \
./editions/xlsx-utils \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/editions/xlsx-utils/ \
- --build index \
+ --build external \
|| exit 1
# /editions/resumebuilder/index.html Resume builder edition
node $TW5_BUILD_TIDDLYWIKI \
./editions/resumebuilder \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/editions/resumebuilder/ \
--build index \
@@ -191,16 +213,14 @@ node $TW5_BUILD_TIDDLYWIKI \
# /editions/text-slicer/index.html Text slicer edition
node $TW5_BUILD_TIDDLYWIKI \
./editions/text-slicer \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/editions/text-slicer/ \
- --build index \
+ --build external \
|| exit 1
# /editions/translators/index.html Translators edition
node $TW5_BUILD_TIDDLYWIKI \
./editions/translators \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/editions/translators/ \
--build index \
@@ -209,7 +229,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /editions/introduction/index.html Introduction edition
node $TW5_BUILD_TIDDLYWIKI \
./editions/introduction \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/editions/introduction/ \
--build index \
@@ -218,7 +237,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /editions/full/index.html Full edition
node $TW5_BUILD_TIDDLYWIKI \
./editions/full \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/editions/full/ \
--build index \
@@ -227,16 +245,14 @@ node $TW5_BUILD_TIDDLYWIKI \
# /editions/tw5.com-docs/index.html tiddlywiki.com docs edition
node $TW5_BUILD_TIDDLYWIKI \
./editions/tw5.com-docs \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/editions/tw5.com-docs/ \
- --build index \
+ --build external \
|| exit 1
# /editions/twitter-archivist/index.html Twitter Archivist edition
node $TW5_BUILD_TIDDLYWIKI \
./editions/twitter-archivist \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/editions/twitter-archivist/ \
--build index \
@@ -252,10 +268,9 @@ node $TW5_BUILD_TIDDLYWIKI \
node $TW5_BUILD_TIDDLYWIKI \
./editions/innerwikidemo \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
- --rendertiddler $:/core/save/all plugins/tiddlywiki/innerwiki/index.html text/plain \
+ --rendertiddler $:/core/save/all-external-js plugins/tiddlywiki/innerwiki/index.html text/plain \
|| exit 1
# /plugins/tiddlywiki/dynaview/index.html Demo wiki with DynaView plugin
@@ -263,10 +278,9 @@ node $TW5_BUILD_TIDDLYWIKI \
node $TW5_BUILD_TIDDLYWIKI \
./editions/dynaviewdemo \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
- --rendertiddler $:/core/save/all plugins/tiddlywiki/dynaview/index.html text/plain \
+ --rendertiddler $:/core/save/all-external-js plugins/tiddlywiki/dynaview/index.html text/plain \
--rendertiddler $:/core/save/empty plugins/tiddlywiki/dynaview/empty.html text/plain \
|| exit 1
@@ -278,43 +292,19 @@ node $TW5_BUILD_TIDDLYWIKI \
node $TW5_BUILD_TIDDLYWIKI \
./editions/katexdemo \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
- --rendertiddler $:/core/save/all plugins/tiddlywiki/katex/index.html text/plain \
+ --rendertiddler $:/core/save/all-external-js plugins/tiddlywiki/katex/index.html text/plain \
--rendertiddler $:/core/save/empty plugins/tiddlywiki/katex/empty.html text/plain \
|| exit 1
-# /plugins/tiddlywiki/tahoelafs/index.html Demo wiki with Tahoe-LAFS plugin
-# /plugins/tiddlywiki/tahoelafs/empty.html Empty wiki with Tahoe-LAFS plugin
-node $TW5_BUILD_TIDDLYWIKI \
- ./editions/tahoelafs \
- --verbose \
- --load $TW5_BUILD_OUTPUT/build.tid \
- --output $TW5_BUILD_OUTPUT \
- --rendertiddler $:/core/save/all plugins/tiddlywiki/tahoelafs/index.html text/plain \
- --rendertiddler $:/core/save/empty plugins/tiddlywiki/tahoelafs/empty.html text/plain \
- || exit 1
-
-# /plugins/tiddlywiki/d3/index.html Demo wiki with D3 plugin
-# /plugins/tiddlywiki/d3/empty.html Empty wiki with D3 plugin
-node $TW5_BUILD_TIDDLYWIKI \
- ./editions/d3demo \
- --verbose \
- --load $TW5_BUILD_OUTPUT/build.tid \
- --output $TW5_BUILD_OUTPUT \
- --rendertiddler $:/core/save/all plugins/tiddlywiki/d3/index.html text/plain \
- --rendertiddler $:/core/save/empty plugins/tiddlywiki/d3/empty.html text/plain \
- || exit 1
-
# /plugins/tiddlywiki/codemirror/index.html Demo wiki with codemirror plugin
# /plugins/tiddlywiki/codemirror/empty.html Empty wiki with codemirror plugin
node $TW5_BUILD_TIDDLYWIKI \
./editions/codemirrordemo \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
- --rendertiddler $:/core/save/all plugins/tiddlywiki/codemirror/index.html text/plain \
+ --rendertiddler $:/core/save/all-external-js plugins/tiddlywiki/codemirror/index.html text/plain \
--rendertiddler $:/core/save/empty plugins/tiddlywiki/codemirror/empty.html text/plain \
|| exit 1
@@ -322,10 +312,9 @@ node $TW5_BUILD_TIDDLYWIKI \
# /plugins/tiddlywiki/markdown/empty.html Empty wiki with Markdown plugin
node $TW5_BUILD_TIDDLYWIKI \
./editions/markdowndemo \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
- --rendertiddler $:/core/save/all plugins/tiddlywiki/markdown/index.html text/plain \
+ --rendertiddler $:/core/save/all-external-js plugins/tiddlywiki/markdown/index.html text/plain \
--rendertiddler $:/core/save/empty plugins/tiddlywiki/markdown/empty.html text/plain \
|| exit 1
@@ -333,10 +322,9 @@ node $TW5_BUILD_TIDDLYWIKI \
# /plugins/tiddlywiki/tw2parser/empty.html Empty wiki with tw2parser plugin
node $TW5_BUILD_TIDDLYWIKI \
./editions/classicparserdemo \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
- --rendertiddler $:/core/save/all plugins/tiddlywiki/tw2parser/index.html text/plain \
+ --rendertiddler $:/core/save/all-external-js plugins/tiddlywiki/tw2parser/index.html text/plain \
--rendertiddler $:/core/save/empty plugins/tiddlywiki/tw2parser/empty.html text/plain \
|| exit 1
@@ -344,13 +332,22 @@ node $TW5_BUILD_TIDDLYWIKI \
# /plugins/tiddlywiki/highlight/empty.html Empty wiki with highlight plugin
node $TW5_BUILD_TIDDLYWIKI \
./editions/highlightdemo \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT \
- --rendertiddler $:/core/save/all plugins/tiddlywiki/highlight/index.html text/plain \
+ --rendertiddler $:/core/save/all-external-js plugins/tiddlywiki/highlight/index.html text/plain \
--rendertiddler $:/core/save/empty plugins/tiddlywiki/highlight/empty.html text/plain \
|| exit 1
+# /plugins/tiddlywiki/geospatial/index.html Demo wiki with geospatial plugin
+# /plugins/tiddlywiki/geospatial/empty.html Empty wiki with geospatial plugin
+node $TW5_BUILD_TIDDLYWIKI \
+ ./editions/geospatialdemo \
+ --load $TW5_BUILD_OUTPUT/build.tid \
+ --output $TW5_BUILD_OUTPUT \
+ --rendertiddler $:/core/save/all-external-js plugins/tiddlywiki/geospatial/index.html text/plain \
+ --rendertiddler $:/core/save/empty plugins/tiddlywiki/geospatial/empty.html text/plain \
+ || exit 1
+
######################################################
#
# Language editions
@@ -372,7 +369,6 @@ rm -rf $TW5_BUILD_OUTPUT/languages/zh-Hant/static/*
# /languages/de-AT/empty.html Empty wiki with de-AT language
node $TW5_BUILD_TIDDLYWIKI \
./editions/de-AT \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/languages/de-AT \
--build favicon empty static index \
@@ -382,7 +378,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /languages/de-DE/empty.html Empty wiki with de-DE language
node $TW5_BUILD_TIDDLYWIKI \
./editions/de-DE \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/languages/de-DE \
--build favicon empty static index \
@@ -392,7 +387,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /languages/es-ES/empty.html Empty wiki with es-ES language
node $TW5_BUILD_TIDDLYWIKI \
./editions/es-ES \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/languages/es-ES \
--build favicon empty static index \
@@ -402,7 +396,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /languages/fr-FR/empty.html Empty wiki with fr-FR language
node $TW5_BUILD_TIDDLYWIKI \
./editions/fr-FR \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/languages/fr-FR \
--build favicon empty static index \
@@ -412,7 +405,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /languages/ja-JP/empty.html Empty wiki with ja-JP language
node $TW5_BUILD_TIDDLYWIKI \
./editions/ja-JP \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/languages/ja-JP \
--build empty index \
@@ -422,7 +414,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /languages/ko-KR/empty.html Empty wiki with ko-KR language
node $TW5_BUILD_TIDDLYWIKI \
./editions/ko-KR \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/languages/ko-KR \
--build favicon empty static index \
@@ -432,7 +423,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /languages/zh-Hans/empty.html Empty wiki with zh-Hans language
node $TW5_BUILD_TIDDLYWIKI \
./editions/zh-Hans \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/languages/zh-Hans \
--build empty index \
@@ -442,7 +432,6 @@ node $TW5_BUILD_TIDDLYWIKI \
# /languages/zh-Hant/empty.html Empty wiki with zh-Hant language
node $TW5_BUILD_TIDDLYWIKI \
./editions/zh-Hant \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/languages/zh-Hant \
--build empty index \
@@ -456,7 +445,6 @@ node $TW5_BUILD_TIDDLYWIKI \
node $TW5_BUILD_TIDDLYWIKI \
./editions/pluginlibrary \
- --verbose \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_OUTPUT/library/$TW5_BUILD_VERSION \
--build library\
diff --git a/bin/ci-pre-build.sh b/bin/ci-pre-build.sh
index 6f4b0ca78..a11b8e0c4 100755
--- a/bin/ci-pre-build.sh
+++ b/bin/ci-pre-build.sh
@@ -7,4 +7,4 @@ npm --force install tiddlywiki || exit 1
# Pull existing GitHub pages content
-git clone --depth=1 --branch=master "https://github.com/Jermolene/jermolene.github.io.git" output
+git clone --depth=1 --branch=master "https://github.com/TiddlyWiki/tiddlywiki.com-gh-pages.git" output
diff --git a/bin/ci-push.sh b/bin/ci-push.sh
index dff297c80..fe8373785 100755
--- a/bin/ci-push.sh
+++ b/bin/ci-push.sh
@@ -10,6 +10,6 @@ git config --global user.email "actions@github.com"
git config --global user.name "GitHub Actions"
git add -A .
git commit --message "GitHub build: $GITHUB_RUN_NUMBER of $TW5_BUILD_BRANCH ($(date +'%F %T %Z'))"
-git remote add deploy "https://$GH_TOKEN@github.com/Jermolene/jermolene.github.io.git" &>/dev/null
+git remote add deploy "https://$GH_TOKEN@github.com/TiddlyWiki/tiddlywiki.com-gh-pages.git" &>/dev/null
git push deploy master &>/dev/null
cd ..
diff --git a/bin/ci-test.sh b/bin/ci-test.sh
new file mode 100755
index 000000000..ffcae66b2
--- /dev/null
+++ b/bin/ci-test.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# test TiddlyWiki5 for tiddlywiki.com
+
+node ./tiddlywiki.js \
+ ./editions/test \
+ --verbose \
+ --version \
+ --rendertiddler $:/core/save/all test.html text/plain \
+ --test \
+ || exit 1
+
+npm install playwright @playwright/test
+npx playwright install chromium firefox --with-deps
+
+npx playwright test
diff --git a/bin/clean.sh b/bin/clean.sh
index 522479edb..5a56e1971 100755
--- a/bin/clean.sh
+++ b/bin/clean.sh
@@ -2,4 +2,4 @@
# Remove any output files
-find . -regex "^./editions/[a-z0-9\.-]*/output/.*" -delete
+find . -regex "^./editions/.*/output/.*" -delete
diff --git a/bin/optimise-svgs.js b/bin/optimise-svgs.js
index 4920ab920..34c471354 100755
--- a/bin/optimise-svgs.js
+++ b/bin/optimise-svgs.js
@@ -15,40 +15,40 @@ var fs = require("fs"),
{ optimize } = require("svgo"),
config = {
plugins: [
- 'cleanupAttrs',
- 'removeDoctype',
- 'removeXMLProcInst',
- 'removeComments',
- 'removeMetadata',
- 'removeTitle',
- 'removeDesc',
- 'removeUselessDefs',
- 'removeEditorsNSData',
- 'removeEmptyAttrs',
- 'removeHiddenElems',
- 'removeEmptyText',
- 'removeEmptyContainers',
+ "cleanupAttrs",
+ "removeDoctype",
+ "removeXMLProcInst",
+ "removeComments",
+ "removeMetadata",
+ "removeTitle",
+ "removeDesc",
+ "removeUselessDefs",
+ "removeEditorsNSData",
+ "removeEmptyAttrs",
+ "removeHiddenElems",
+ "removeEmptyText",
+ "removeEmptyContainers",
// 'removeViewBox',
- 'cleanupEnableBackground',
- 'convertStyleToAttrs',
- 'convertColors',
- 'convertPathData',
- 'convertTransform',
- 'removeUnknownsAndDefaults',
- 'removeNonInheritableGroupAttrs',
- 'removeUselessStrokeAndFill',
- 'removeUnusedNS',
- 'cleanupIDs',
- 'cleanupNumericValues',
- 'moveElemsAttrsToGroup',
- 'moveGroupAttrsToElems',
- 'collapseGroups',
+ "cleanupEnableBackground",
+ "convertStyleToAttrs",
+ "convertColors",
+ "convertPathData",
+ "convertTransform",
+ "removeUnknownsAndDefaults",
+ "removeNonInheritableGroupAttrs",
+ "removeUselessStrokeAndFill",
+ "removeUnusedNS",
+ "cleanupIDs",
+ "cleanupNumericValues",
+ "moveElemsAttrsToGroup",
+ "moveGroupAttrsToElems",
+ "collapseGroups",
// 'removeRasterImages',
- 'mergePaths',
- 'convertShapeToPath',
- 'sortAttrs',
+ "mergePaths",
+ "convertShapeToPath",
+ "sortAttrs",
//'removeDimensions',
- {name: 'removeAttrs', params: { attrs: '(stroke|fill)' } }
+ {name: "removeAttrs", params: { attrs: "(stroke|fill)" } }
]
};
@@ -72,7 +72,7 @@ files.forEach(function(filename) {
var newSVG = header.join("\n") + "\n\n" + result.data.replace("<<now "DD">>","<>");
fs.writeFileSync(filepath,newSVG);
} else {
- console.log("Error " + err + " with " + filename)
+ console.log("Error " + err + " with " + filename);
process.exit();
};
}
diff --git a/bin/readme-bld.sh b/bin/readme-bld.sh
index 198c3abd0..e7c9df564 100755
--- a/bin/readme-bld.sh
+++ b/bin/readme-bld.sh
@@ -15,3 +15,11 @@ node $TW5_BUILD_TIDDLYWIKI \
--output . \
--build readmes \
|| exit 1
+
+# tw.org readmes
+node $TW5_BUILD_TIDDLYWIKI \
+ editions/tw.org \
+ --verbose \
+ --output . \
+ --build readmes \
+ || exit 1
diff --git a/boot/boot.js b/boot/boot.js
index 9da1d7f60..e3595a460 100644
--- a/boot/boot.js
+++ b/boot/boot.js
@@ -8,10 +8,10 @@ On the server this file is executed directly to boot TiddlyWiki. In the browser,
\*/
+/* eslint-disable @stylistic/indent */
+
var _boot = (function($tw) {
-/*jslint node: true, browser: true */
-/*global modules: false, $tw: false */
"use strict";
// Include bootprefix if we're not given module data
@@ -35,7 +35,7 @@ if($tw.node) {
$tw.boot.log = function(str) {
$tw.boot.logMessages = $tw.boot.logMessages || [];
$tw.boot.logMessages.push(str);
-}
+};
/*
Check if an object has a property
@@ -44,12 +44,8 @@ $tw.utils.hop = function(object,property) {
return object ? Object.prototype.hasOwnProperty.call(object,property) : false;
};
-/*
-Determine if a value is an array
-*/
-$tw.utils.isArray = function(value) {
- return Object.prototype.toString.call(value) == "[object Array]";
-};
+/** @deprecated Use Array.isArray instead */
+$tw.utils.isArray = (value) => Array.isArray(value);
/*
Check if an array is equal by value and by reference.
@@ -128,35 +124,22 @@ $tw.utils.pushTop = function(array,value) {
return array;
};
-/*
-Determine if a value is a date
-*/
-$tw.utils.isDate = function(value) {
- return Object.prototype.toString.call(value) === "[object Date]";
-};
+/** @deprecated Use instanceof Date instead */
+$tw.utils.isDate = (value) => value instanceof Date;
-/*
-Iterate through all the own properties of an object or array. Callback is invoked with (element,title,object)
-*/
+/** @deprecated Use array iterative methods instead */
$tw.utils.each = function(object,callback) {
- var next,f,length;
if(object) {
- if(Object.prototype.toString.call(object) == "[object Array]") {
- for (f=0, length=object.length; f {
+ const next = callback(element,index,array);
+ return next !== false;
+ });
} else {
- var keys = Object.keys(object);
- for (f=0, length=keys.length; f {
+ const next = callback(entry[1], entry[0], object);
+ return next !== false;
+ });
}
}
};
@@ -177,6 +160,7 @@ document: defaults to current document
eventListeners: array of event listeners (this option won't work until $tw.utils.addEventListeners() has been loaded)
*/
$tw.utils.domMaker = function(tag,options) {
+ var options = options || {};
var doc = options.document || document;
var element = doc.createElementNS(options.namespace || "http://www.w3.org/1999/xhtml",tag);
if(options["class"]) {
@@ -218,9 +202,34 @@ $tw.utils.error = function(err) {
heading = dm("h1",{text: errHeading}),
prompt = dm("div",{text: promptMsg, "class": "tc-error-prompt"}),
message = dm("div",{text: err, "class":"tc-error-message"}),
- button = dm("div",{children: [dm("button",{text: ( $tw.language == undefined ? "close" : $tw.language.getString("Buttons/Close/Caption") )})], "class": "tc-error-prompt"}),
- form = dm("form",{children: [heading,prompt,message,button], "class": "tc-error-form"});
+ closeButton = dm("div",{children: [dm("button",{text: ( $tw.language == undefined ? "close" : $tw.language.getString("Buttons/Close/Caption") )})], "class": "tc-error-prompt"}),
+ downloadButton = dm("div",{children: [dm("button",{text: ( $tw.language == undefined ? "download tiddlers" : $tw.language.getString("Buttons/EmergencyDownload/Caption") )})], "class": "tc-error-prompt"}),
+ form = dm("form",{children: [heading,prompt,downloadButton,message,closeButton], "class": "tc-error-form"});
document.body.insertBefore(form,document.body.firstChild);
+ downloadButton.addEventListener("click",function(event) {
+ if($tw && $tw.wiki) {
+ var tiddlers = [];
+ $tw.wiki.each(function(tiddler,title) {
+ tiddlers.push(tiddler.fields);
+ });
+ var link = dm("a"),
+ text = JSON.stringify(tiddlers);
+ if(Blob !== undefined) {
+ var blob = new Blob([text], {type: "application/json"});
+ link.setAttribute("href", URL.createObjectURL(blob));
+ } else {
+ link.setAttribute("href","data:application/json," + encodeURIComponent(text));
+ }
+ link.setAttribute("download","emergency-tiddlers-" + (new Date()) + ".json");
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ } else {
+ alert("Emergency tiddler download is not available");
+ }
+ event.preventDefault();
+ return false;
+ },true);
form.addEventListener("submit",function(event) {
document.body.removeChild(form);
event.preventDefault();
@@ -249,7 +258,7 @@ Extend an object with the properties from a list of source objects
$tw.utils.extend = function(object /*, sourceObjectList */) {
$tw.utils.each(Array.prototype.slice.call(arguments,1),function(source) {
if(source) {
- for (var p in source) {
+ for(var p in source) {
object[p] = source[p];
}
}
@@ -263,7 +272,7 @@ Fill in any null or undefined properties of an object with the properties from a
$tw.utils.deepDefaults = function(object /*, sourceObjectList */) {
$tw.utils.each(Array.prototype.slice.call(arguments,1),function(source) {
if(source) {
- for (var p in source) {
+ for(var p in source) {
if(object[p] === null || object[p] === undefined) {
object[p] = source[p];
}
@@ -309,28 +318,26 @@ $tw.utils.htmlDecode = function(s) {
Get the browser location.hash. We don't use location.hash because of the way that Firefox auto-urldecodes it (see http://stackoverflow.com/questions/1703552/encoding-of-window-location-hash)
*/
$tw.utils.getLocationHash = function() {
- var href = window.location.href;
- var idx = href.indexOf('#');
+ const href = window.location.href,
+ idx = href.indexOf("#");
+
if(idx === -1) {
return "#";
- } else if(href.substr(idx + 1,1) === "#" || href.substr(idx + 1,3) === "%23") {
+ }
+
+ const afterHash = href.substring(idx + 1);
+ if(afterHash.startsWith("#") || afterHash.startsWith("%23")) {
// Special case: ignore location hash if it itself starts with a #
return "#";
- } else {
- return href.substring(idx);
}
+ return href.substring(idx);
};
-/*
-Pad a string to a given length with "0"s. Length defaults to 2
-*/
-$tw.utils.pad = function(value,length) {
- length = length || 2;
- var s = value.toString();
- if(s.length < length) {
- s = "000000000000000000000000000".substr(0,length - s.length) + s;
- }
- return s;
+
+/** @deprecated Pad a string to a given length with "0"s. Length defaults to 2 */
+$tw.utils.pad = function(value,length = 2) {
+ const s = value.toString();
+ return s.padStart(length, "0");
};
// Convert a date into UTC YYYYMMDDHHMMSSmmm format
@@ -360,8 +367,8 @@ $tw.utils.parseDate = function(value) {
parseInt(value.substr(10,2)||"00",10),
parseInt(value.substr(12,2)||"00",10),
parseInt(value.substr(14,3)||"000",10)));
- d.setUTCFullYear(year); // See https://stackoverflow.com/a/5870822
- return d;
+ d.setUTCFullYear(year); // See https://stackoverflow.com/a/5870822
+ return d;
} else if($tw.utils.isDate(value)) {
return value;
} else {
@@ -556,7 +563,7 @@ using a lowercase extension only.
*/
$tw.utils.getFileExtensionInfo = function(ext) {
return ext ? $tw.config.fileExtensionInfo[ext.toLowerCase()] : null;
-}
+};
/*
Given an extension, get the correct encoding for that file.
@@ -569,10 +576,22 @@ $tw.utils.getTypeEncoding = function(ext) {
return typeInfo ? typeInfo.encoding : "utf8";
};
+var globalCheck =[
+ " Object.defineProperty(Object.prototype, '__temp__', {",
+ " get: function () { return this; },",
+ " configurable: true",
+ " });",
+ " if(Object.keys(__temp__).length){",
+ " console.log(\"Warning: Global assignment detected\",Object.keys(__temp__));",
+ " delete Object.prototype.__temp__;",
+ " }",
+ " delete Object.prototype.__temp__;",
+].join("\n");
+
/*
Run code globally with specified context variables in scope
*/
-$tw.utils.evalGlobal = function(code,context,filename) {
+$tw.utils.evalGlobal = function(code,context,filename,sandbox,allowGlobals) {
var contextCopy = $tw.utils.extend(Object.create(null),context);
// Get the context variables as a pair of arrays of names and values
var contextNames = [], contextValues = [];
@@ -581,25 +600,38 @@ $tw.utils.evalGlobal = function(code,context,filename) {
contextValues.push(value);
});
// Add the code prologue and epilogue
- code = "(function(" + contextNames.join(",") + ") {(function(){\n" + code + "\n;})();\nreturn exports;\n})\n";
+ code = [
+ "(function(" + contextNames.join(",") + ") {",
+ " (function(){" + code + "\n;})();\n",
+ (!$tw.browser && sandbox && !allowGlobals) ? globalCheck : "",
+ "\nreturn exports;\n",
+ "})"
+ ].join("");
+
// Compile the code into a function
var fn;
if($tw.browser) {
- fn = window["eval"](code + "\n\n//# sourceURL=" + filename);
+ fn = Function("return " + code + "\n\n//# sourceURL=" + filename)(); // See https://github.com/TiddlyWiki/TiddlyWiki5/issues/6839
} else {
- fn = vm.runInThisContext(code,filename);
+ if(sandbox){
+ fn = vm.runInContext(code,sandbox,filename);
+ } else {
+ fn = vm.runInThisContext(code,filename);
+ }
}
// Call the function and return the exports
return fn.apply(null,contextValues);
};
-
+$tw.utils.sandbox = !$tw.browser ? vm.createContext({}) : undefined;
/*
Run code in a sandbox with only the specified context variables in scope
*/
-$tw.utils.evalSandboxed = $tw.browser ? $tw.utils.evalGlobal : function(code,context,filename) {
- var sandbox = $tw.utils.extend(Object.create(null),context);
- vm.runInNewContext(code,sandbox,filename);
- return sandbox.exports;
+$tw.utils.evalSandboxed = $tw.browser ? $tw.utils.evalGlobal : function(code,context,filename,allowGlobals) {
+ return $tw.utils.evalGlobal(
+ code,context,filename,
+ allowGlobals ? vm.createContext({}) : $tw.utils.sandbox,
+ allowGlobals
+ );
};
/*
@@ -693,7 +725,7 @@ $tw.utils.PasswordPrompt.prototype.createPrompt = function(options) {
var self = this;
form.addEventListener("submit",function(event) {
// Collect the form data
- var data = {},t;
+ var data = {};
$tw.utils.each(form.elements,function(element) {
if(element.name && element.value) {
data[element.name] = element.value;
@@ -739,7 +771,7 @@ $tw.utils.PasswordPrompt.prototype.removePrompt = function(promptInfo) {
promptInfo.form.parentNode.removeChild(promptInfo.form);
this.setWrapperDisplay();
}
-}
+};
/*
Crypto helper object for encrypted content. It maintains the password text in a closure, and provides methods to change
@@ -748,12 +780,13 @@ the password, and to encrypt/decrypt a block of text
$tw.utils.Crypto = function() {
var sjcl = $tw.node ? (global.sjcl || require("./sjcl.js")) : window.sjcl,
currentPassword = null,
- callSjcl = function(method,inputText,password) {
+ callSjcl = function(method,inputText,password,options) {
+ options = options || {};
password = password || currentPassword;
var outputText;
try {
if(password) {
- outputText = sjcl[method](password,inputText);
+ outputText = sjcl[method](password,inputText,options);
}
} catch(ex) {
console.log("Crypto error:" + ex);
@@ -761,6 +794,7 @@ $tw.utils.Crypto = function() {
}
return outputText;
};
+ $tw.sjcl = sjcl;
this.setPassword = function(newPassword) {
currentPassword = newPassword;
this.updateCryptoStateTiddler();
@@ -776,9 +810,10 @@ $tw.utils.Crypto = function() {
};
this.hasPassword = function() {
return !!currentPassword;
- }
+ };
this.encrypt = function(text,password) {
- return callSjcl("encrypt",text,password);
+ // set default ks:256 -- see: http://bitwiseshiftleft.github.io/sjcl/doc/convenience.js.html
+ return callSjcl("encrypt",text,password,{v:1,iter:10000,ks:256,ts:64,mode:"ccm",adata:"",cipher:"aes"});
};
this.decrypt = function(text,password) {
return callSjcl("decrypt",text,password);
@@ -793,7 +828,7 @@ Execute the module named 'moduleName'. The name can optionally be relative to th
$tw.modules.execute = function(moduleName,moduleRoot) {
var name = moduleName;
if(moduleName.charAt(0) === ".") {
- name = $tw.utils.resolvePath(moduleName,moduleRoot)
+ name = $tw.utils.resolvePath(moduleName,moduleRoot);
}
if(!$tw.modules.titles[name]) {
if($tw.modules.titles[name + ".js"]) {
@@ -841,8 +876,8 @@ $tw.modules.execute = function(moduleName,moduleRoot) {
} else {
/*
CommonJS optional require.main property:
- In a browser we offer a fake main module which points back to the boot function
- (Theoretically, this may allow TW to eventually load itself as a module in the browser)
+ In a browser we offer a fake main module which points back to the boot function
+ (Theoretically, this may allow TW to eventually load itself as a module in the browser)
*/
Object.defineProperty(sandbox.require, "main", {
value: (typeof(require) !== "undefined") ? require.main : {TiddlyWiki: _boot},
@@ -854,7 +889,6 @@ $tw.modules.execute = function(moduleName,moduleRoot) {
if(!moduleInfo) {
// We could not find the module on this path
// Try to defer to browserify etc, or node
- var deferredModule;
if($tw.browser) {
if(window.require) {
try {
@@ -884,9 +918,9 @@ $tw.modules.execute = function(moduleName,moduleRoot) {
moduleInfo.exports = moduleInfo.definition;
}
} catch(e) {
- if (e instanceof SyntaxError) {
+ if(e instanceof SyntaxError) {
var line = e.lineNumber || e.line; // Firefox || Safari
- if (typeof(line) != "undefined" && line !== null) {
+ if(typeof(line) != "undefined" && line !== null) {
$tw.utils.error("Syntax error in boot module " + name + ":" + line + ":\n" + e.stack);
} else if(!$tw.browser) {
// this is the only way to get node.js to display the line at which the syntax error appeared,
@@ -900,7 +934,7 @@ $tw.modules.execute = function(moduleName,moduleRoot) {
}
} else {
// line number should be included in e.stack for runtime errors
- $tw.utils.error("Error executing boot module " + name + ": " + JSON.stringify(e) + "\n\n" + e.stack);
+ $tw.utils.error("Error executing boot module " + name + ": " + String(e) + "\n\n" + e.stack);
}
}
}
@@ -1109,8 +1143,7 @@ enableIndexers - Array of indexer names to enable, or null to use all available
*/
$tw.Wiki = function(options) {
options = options || {};
- var self = this,
- tiddlers = Object.create(null), // Hashmap of tiddlers
+ var tiddlers = Object.create(null), // Hashmap of tiddlers
tiddlerTitles = null, // Array of tiddler titles
getTiddlerTitles = function() {
if(!tiddlerTitles) {
@@ -1124,7 +1157,7 @@ $tw.Wiki = function(options) {
shadowTiddlerTitles = null,
getShadowTiddlerTitles = function() {
if(!shadowTiddlerTitles) {
- shadowTiddlerTitles = Object.keys(shadowTiddlers);
+ shadowTiddlerTitles = Object.keys(shadowTiddlers).sort(function(a,b) {return a.localeCompare(b);});
}
return shadowTiddlerTitles;
},
@@ -1164,7 +1197,7 @@ $tw.Wiki = function(options) {
shadow: this.isShadowTiddler(title),
exists: this.tiddlerExists(title)
}
- }
+ };
// Save the new tiddler
tiddlers[title] = tiddler;
// Check we've got the title
@@ -1174,7 +1207,7 @@ $tw.Wiki = function(options) {
tiddler: tiddler,
shadow: this.isShadowTiddler(title),
exists: this.tiddlerExists(title)
- }
+ };
// Update indexes
this.clearCache(title);
this.clearGlobalCache();
@@ -1199,7 +1232,7 @@ $tw.Wiki = function(options) {
shadow: this.isShadowTiddler(title),
exists: this.tiddlerExists(title)
}
- }
+ };
// Delete the tiddler
delete tiddlers[title];
// Delete it from the list of titles
@@ -1214,7 +1247,7 @@ $tw.Wiki = function(options) {
tiddler: this.getTiddler(title),
shadow: this.isShadowTiddler(title),
exists: this.tiddlerExists(title)
- }
+ };
// Update indexes
this.clearCache(title);
this.clearGlobalCache();
@@ -1381,7 +1414,7 @@ $tw.Wiki = function(options) {
checkTiddler = function(tiddler,title) {
if(tiddler && tiddler.fields.type === "application/json" && tiddler.fields["plugin-type"] && (!pluginType || tiddler.fields["plugin-type"] === pluginType)) {
var disablingTiddler = self.getTiddler("$:/config/Plugins/Disabled/" + title);
- if(title === "$:/core" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") {
+ if(title === "$:/core" || title === "$:/core-server" || !disablingTiddler || (disablingTiddler.fields.text || "").trim() !== "yes") {
self.unregisterPluginTiddlers(null,[title]); // Unregister the plugin if it's already registered
pluginTiddlers.push(tiddler);
registeredTitles.push(tiddler.fields.title);
@@ -1402,8 +1435,7 @@ $tw.Wiki = function(options) {
// Unregister the plugin tiddlers of a particular type, or null/undefined for any type, optionally restricting unregistering to an array of tiddler titles. Returns an array of the titles affected
this.unregisterPluginTiddlers = function(pluginType,titles) {
- var self = this,
- unregisteredTitles = [];
+ var unregisteredTitles = [];
// Remove any previous registered plugins of this type
for(var t=pluginTiddlers.length-1; t>=0; t--) {
var tiddler = pluginTiddlers[t];
@@ -1417,15 +1449,12 @@ $tw.Wiki = function(options) {
// Unpack the currently registered plugins, creating shadow tiddlers for their constituent tiddlers
this.unpackPluginTiddlers = function() {
- var self = this;
- // Sort the plugin titles by the `plugin-priority` field
- pluginTiddlers.sort(function(a,b) {
- if("plugin-priority" in a.fields && "plugin-priority" in b.fields) {
- return a.fields["plugin-priority"] - b.fields["plugin-priority"];
- } else if("plugin-priority" in a.fields) {
- return -1;
- } else if("plugin-priority" in b.fields) {
- return +1;
+ // Sort the plugin titles by the `plugin-priority` field, if this field is missing, default to 1
+ pluginTiddlers.sort(function(a, b) {
+ var priorityA = "plugin-priority" in a.fields ? a.fields["plugin-priority"] : 1;
+ var priorityB = "plugin-priority" in b.fields ? b.fields["plugin-priority"] : 1;
+ if(priorityA !== priorityB) {
+ return priorityA - priorityB;
} else if(a.fields.title < b.fields.title) {
return -1;
} else if(a.fields.title === b.fields.title) {
@@ -1480,8 +1509,9 @@ Define all modules stored in ordinary tiddlers
*/
$tw.Wiki.prototype.defineTiddlerModules = function() {
this.each(function(tiddler,title) {
- if(tiddler.hasField("module-type")) {
- switch (tiddler.fields.type) {
+ // Modules in draft tiddlers are disabled
+ if(tiddler.hasField("module-type") && (!tiddler.hasField("draft.of"))) {
+ switch(tiddler.fields.type) {
case "application/javascript":
// We only define modules that haven't already been defined, because in the browser modules in system tiddlers are defined in inline script
if(!$tw.utils.hop($tw.modules.titles,tiddler.fields.title)) {
@@ -1507,6 +1537,11 @@ $tw.Wiki.prototype.defineShadowModules = function() {
this.eachShadow(function(tiddler,title) {
// Don't define the module if it is overidden by an ordinary tiddler
if(!self.tiddlerExists(title) && tiddler.hasField("module-type")) {
+ if(tiddler.hasField("draft.of")) {
+ // Report a fundamental problem
+ console.warn(`TiddlyWiki: Plugins should not contain tiddlers with a 'draft.of' field: ${tiddler.fields.title}`);
+ return;
+ }
// Define the module
$tw.modules.define(tiddler.fields.title,tiddler.fields["module-type"],tiddler.fields.text);
}
@@ -1529,7 +1564,7 @@ $tw.Wiki.prototype.processSafeMode = function() {
// Assemble a report tiddler
var titleReportTiddler = "TiddlyWiki Safe Mode",
report = [];
- report.push("TiddlyWiki has been started in [[safe mode|https://tiddlywiki.com/static/SafeMode.html]]. All plugins are temporarily disabled. Most customisations have been disabled by renaming the following tiddlers:")
+ report.push("TiddlyWiki has been started in [[safe mode|https://tiddlywiki.com/static/SafeMode.html]]. All plugins are temporarily disabled. Most customisations have been disabled by renaming the following tiddlers:");
// Delete the overrides
overrides.forEach(function(title) {
var tiddler = self.getTiddler(title),
@@ -1538,7 +1573,7 @@ $tw.Wiki.prototype.processSafeMode = function() {
self.addTiddler(new $tw.Tiddler(tiddler, {title: newTitle}));
report.push("* [[" + title + "|" + newTitle + "]]");
});
- report.push()
+ report.push();
this.addTiddler(new $tw.Tiddler({title: titleReportTiddler, text: report.join("\n\n")}));
// Set $:/DefaultTiddlers to point to our report
this.addTiddler(new $tw.Tiddler({title: "$:/DefaultTiddlers", text: "[[" + titleReportTiddler + "]]"}));
@@ -1854,8 +1889,16 @@ $tw.loadTiddlersFromFile = function(filepath,fields) {
extensionInfo = $tw.utils.getFileExtensionInfo(ext),
type = extensionInfo ? extensionInfo.type : null,
typeInfo = type ? $tw.config.contentTypeInfo[type] : null,
- data = fs.readFileSync(filepath,typeInfo ? typeInfo.encoding : "utf8"),
- tiddlers = $tw.wiki.deserializeTiddlers(ext,data,fields),
+ fileSize = fs.statSync(filepath).size,
+ data;
+ if(fileSize > $tw.config.maxEditFileSize) {
+ data = "File " + filepath + " not loaded because it is too large";
+ console.log("Warning: " + data);
+ ext = ".txt";
+ } else {
+ data = fs.readFileSync(filepath,typeInfo ? typeInfo.encoding : "utf8");
+ }
+ var tiddlers = $tw.wiki.deserializeTiddlers(ext,data,fields),
metadata = $tw.loadMetadataForFile(filepath);
if(metadata) {
if(type === "application/json") {
@@ -1920,22 +1963,41 @@ filepath: pathname of the directory containing the specification file
$tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
var tiddlers = [];
// Read the specification
- var filesInfo = $tw.utils.parseJSONSafe(fs.readFileSync(filepath + path.sep + "tiddlywiki.files","utf8"));
+ var filesInfo = $tw.utils.parseJSONSafe(fs.readFileSync(filepath + path.sep + "tiddlywiki.files","utf8"), function(e) {
+ console.log("Warning: tiddlywiki.files in " + filepath + " invalid: " + e.message);
+ return {};
+ });
+
// Helper to process a file
- var processFile = function(filename,isTiddlerFile,fields,isEditableFile) {
+ var processFile = function(filename,isTiddlerFile,fields,isEditableFile,rootPath) {
var extInfo = $tw.config.fileExtensionInfo[path.extname(filename)],
type = (extInfo || {}).type || fields.type || "text/plain",
typeInfo = $tw.config.contentTypeInfo[type] || {},
pathname = path.resolve(filepath,filename),
- text = fs.readFileSync(pathname,typeInfo.encoding || "utf8"),
metadata = $tw.loadMetadataForFile(pathname) || {},
- fileTiddlers;
+ fileTooLarge = false,
+ text, fileTiddlers;
+
+ if("_canonical_uri" in fields) {
+ text = "";
+ } else if(fs.statSync(pathname).size > $tw.config.maxEditFileSize) {
+ var msg = "File " + pathname + " not loaded because it is too large";
+ console.log("Warning: " + msg);
+ fileTooLarge = true;
+ text = isTiddlerFile ? msg : "";
+ } else {
+ text = fs.readFileSync(pathname,typeInfo.encoding || "utf8");
+ }
+
if(isTiddlerFile) {
- fileTiddlers = $tw.wiki.deserializeTiddlers(path.extname(pathname),text,metadata) || [];
+ fileTiddlers = $tw.wiki.deserializeTiddlers(fileTooLarge ? ".txt" : path.extname(pathname),text,metadata) || [];
} else {
fileTiddlers = [$tw.utils.extend({text: text},metadata)];
}
var combinedFields = $tw.utils.extend({},fields,metadata);
+ if(fileTooLarge && isTiddlerFile) {
+ delete combinedFields.type; // type altered
+ }
$tw.utils.each(fileTiddlers,function(tiddler) {
$tw.utils.each(combinedFields,function(fieldInfo,name) {
if(typeof fieldInfo === "string" || $tw.utils.isArray(fieldInfo)) {
@@ -1943,6 +2005,12 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
} else {
var value = tiddler[name];
switch(fieldInfo.source) {
+ case "subdirectories":
+ value = $tw.utils.stringifyList(path.relative(rootPath, filename).split(path.sep).slice(0, -1));
+ break;
+ case "filepath":
+ value = path.relative(rootPath, filename).split(path.sep).join("/");
+ break;
case "filename":
value = path.basename(filename);
break;
@@ -1959,10 +2027,10 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
value = path.extname(filename);
break;
case "created":
- value = new Date(fs.statSync(pathname).birthtime);
+ value = $tw.utils.stringifyDate(new Date(fs.statSync(pathname).birthtime));
break;
case "modified":
- value = new Date(fs.statSync(pathname).mtime);
+ value = $tw.utils.stringifyDate(new Date(fs.statSync(pathname).mtime));
break;
}
if(fieldInfo.prefix) {
@@ -1987,14 +2055,14 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
arrayOfFiles = arrayOfFiles || [];
var files = fs.readdirSync(dirPath);
files.forEach(function(file) {
- if (recurse && fs.statSync(dirPath + path.sep + file).isDirectory()) {
+ if(recurse && fs.statSync(dirPath + path.sep + file).isDirectory()) {
arrayOfFiles = getAllFiles(dirPath + path.sep + file, recurse, arrayOfFiles);
} else if(fs.statSync(dirPath + path.sep + file).isFile()){
arrayOfFiles.push(path.join(dirPath, path.sep, file));
}
});
return arrayOfFiles;
- }
+ };
// Process the listed tiddlers
$tw.utils.each(filesInfo.tiddlers,function(tidInfo) {
if(tidInfo.prefix && tidInfo.suffix) {
@@ -2004,6 +2072,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
} else if(tidInfo.suffix) {
tidInfo.fields.text = {suffix: tidInfo.suffix};
}
+ tidInfo.fields = tidInfo.fields || {};
processFile(tidInfo.file,tidInfo.isTiddlerFile,tidInfo.fields);
});
// Process any listed directories
@@ -2025,7 +2094,8 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
var thisPath = path.relative(filepath, files[t]),
filename = path.basename(thisPath);
if(filename !== "tiddlywiki.files" && !metaRegExp.test(filename) && fileRegExp.test(filename)) {
- processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile);
+ dirSpec.fields = dirSpec.fields || {};
+ processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile,dirSpec.path);
}
}
} else {
@@ -2050,7 +2120,11 @@ $tw.loadPluginFolder = function(filepath,excludeRegExp) {
console.log("Warning: missing plugin.info file in " + filepath);
return null;
}
- var pluginInfo = $tw.utils.parseJSONSafe(fs.readFileSync(infoPath,"utf8"));
+ var pluginInfo = $tw.utils.parseJSONSafe(fs.readFileSync(infoPath,"utf8"),function() {return null;});
+ if(!pluginInfo) {
+ console.log("warning: invalid JSON in plugin.info file at " + infoPath);
+ pluginInfo = {};
+ }
// Read the plugin files
var pluginFiles = $tw.loadTiddlersFromPath(filepath,excludeRegExp);
// Save the plugin tiddlers into the plugin info
@@ -2097,7 +2171,7 @@ Returns the path of the plugin folder
$tw.findLibraryItem = function(name,paths) {
var pathIndex = 0;
do {
- var pluginPath = path.resolve(paths[pathIndex],"./" + name)
+ var pluginPath = path.resolve(paths[pathIndex],"./" + name);
if(fs.existsSync(pluginPath) && fs.statSync(pluginPath).isDirectory()) {
return pluginPath;
}
@@ -2128,13 +2202,16 @@ Returns an array of search paths
*/
$tw.getLibraryItemSearchPaths = function(libraryPath,envVar) {
var pluginPaths = [path.resolve($tw.boot.corePath,libraryPath)],
+ env;
+ if(envVar) {
env = process.env[envVar];
- if(env) {
- env.split(path.delimiter).map(function(item) {
- if(item) {
- pluginPaths.push(item);
- }
- });
+ if(env) {
+ env.split(path.delimiter).map(function(item) {
+ if(item) {
+ pluginPaths.push(item);
+ }
+ });
+ }
}
return pluginPaths;
};
@@ -2167,7 +2244,11 @@ $tw.loadWikiTiddlers = function(wikiPath,options) {
pluginFields;
// Bail if we don't have a wiki info file
if(fs.existsSync(wikiInfoPath)) {
- wikiInfo = $tw.utils.parseJSONSafe(fs.readFileSync(wikiInfoPath,"utf8"));
+ wikiInfo = $tw.utils.parseJSONSafe(fs.readFileSync(wikiInfoPath,"utf8"),function() {return null;});
+ if(!wikiInfo) {
+ console.log("warning: invalid JSON in tiddlywiki.info file at " + wikiInfoPath);
+ wikiInfo = {};
+ }
} else {
return null;
}
@@ -2216,7 +2297,7 @@ $tw.loadWikiTiddlers = function(wikiPath,options) {
}
$tw.wiki.addTiddlers(tiddlerFile.tiddlers);
});
- if ($tw.boot.wikiPath == wikiPath) {
+ if($tw.boot.wikiPath == wikiPath) {
// Save the original tiddler file locations if requested
var output = {}, relativePath, fileInfo;
for(var title in $tw.boot.files) {
@@ -2277,6 +2358,7 @@ $tw.loadTiddlersNode = function() {
});
// Load the core tiddlers
$tw.wiki.addTiddler($tw.loadPluginFolder($tw.boot.corePath));
+ $tw.wiki.addTiddler($tw.loadPluginFolder($tw.boot.coreServerPath));
// Load any extra plugins
$tw.utils.each($tw.boot.extraPlugins,function(name) {
if(name.charAt(0) === "+") { // Relative path to plugin
@@ -2350,6 +2432,7 @@ $tw.boot.initStartup = function(options) {
// System paths and filenames
$tw.boot.bootPath = options.bootPath || path.dirname(module.filename);
$tw.boot.corePath = path.resolve($tw.boot.bootPath,"../core");
+ $tw.boot.coreServerPath = path.resolve($tw.boot.bootPath,"../core-server");
// If there's no arguments then default to `--help`
if($tw.boot.argv.length === 0) {
$tw.boot.argv = ["--help"];
@@ -2398,25 +2481,32 @@ $tw.boot.initStartup = function(options) {
$tw.utils.registerFileType("image/webp","base64",".webp",{flags:["image"]});
$tw.utils.registerFileType("image/heic","base64",".heic",{flags:["image"]});
$tw.utils.registerFileType("image/heif","base64",".heif",{flags:["image"]});
+ $tw.utils.registerFileType("image/avif","base64",".avif",{flags:["image"]});
$tw.utils.registerFileType("image/svg+xml","utf8",".svg",{flags:["image"]});
$tw.utils.registerFileType("image/vnd.microsoft.icon","base64",".ico",{flags:["image"]});
$tw.utils.registerFileType("image/x-icon","base64",".ico",{flags:["image"]});
- $tw.utils.registerFileType("application/font-woff","base64",".woff");
- $tw.utils.registerFileType("application/x-font-ttf","base64",".woff");
- $tw.utils.registerFileType("application/font-woff2","base64",".woff2");
+ $tw.utils.registerFileType("application/wasm","base64",".wasm");
+ $tw.utils.registerFileType("font/woff","base64",".woff");
+ $tw.utils.registerFileType("font/woff2","base64",".woff2");
+ $tw.utils.registerFileType("font/ttf","base64",".ttf");
+ $tw.utils.registerFileType("font/otf","base64",".otf");
$tw.utils.registerFileType("audio/ogg","base64",".ogg");
$tw.utils.registerFileType("audio/mp4","base64",[".mp4",".m4a"]);
$tw.utils.registerFileType("video/ogg","base64",[".ogm",".ogv",".ogg"]);
$tw.utils.registerFileType("video/webm","base64",".webm");
$tw.utils.registerFileType("video/mp4","base64",".mp4");
$tw.utils.registerFileType("audio/mp3","base64",".mp3");
- $tw.utils.registerFileType("audio/mpeg","base64");
+ $tw.utils.registerFileType("audio/mpeg","base64",[".mp3",".m2a",".mp2",".mpa",".mpg",".mpga"]);
$tw.utils.registerFileType("text/markdown","utf8",[".md",".markdown"],{deserializerType:"text/x-markdown"});
$tw.utils.registerFileType("text/x-markdown","utf8",[".md",".markdown"]);
$tw.utils.registerFileType("application/enex+xml","utf8",".enex");
$tw.utils.registerFileType("application/vnd.openxmlformats-officedocument.wordprocessingml.document","base64",".docx");
+ $tw.utils.registerFileType("application/msword","base64",".doc");
$tw.utils.registerFileType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet","base64",".xlsx");
+ $tw.utils.registerFileType("application/excel","base64",".xls");
+ $tw.utils.registerFileType("application/vnd.ms-excel","base64",".xls");
$tw.utils.registerFileType("application/vnd.openxmlformats-officedocument.presentationml.presentation","base64",".pptx");
+ $tw.utils.registerFileType("application/mspowerpoint","base64",".ppt");
$tw.utils.registerFileType("text/x-bibtex","utf8",".bib",{deserializerType:"application/x-bibtex"});
$tw.utils.registerFileType("application/x-bibtex","utf8",".bib");
$tw.utils.registerFileType("application/epub+zip","base64",".epub");
@@ -2440,7 +2530,7 @@ $tw.boot.initStartup = function(options) {
}
});
return result;
- }
+ };
}
};
$tw.boot.loadStartup = function(options){
@@ -2457,7 +2547,7 @@ $tw.boot.loadStartup = function(options){
}
// Give hooks a chance to modify the store
$tw.hooks.invokeHook("th-boot-tiddlers-loaded");
-}
+};
$tw.boot.execStartup = function(options){
// Unpack plugin tiddlers
$tw.wiki.readPluginInfo();
@@ -2467,10 +2557,10 @@ $tw.boot.execStartup = function(options){
if($tw.safeMode) {
$tw.wiki.processSafeMode();
}
- // Register typed modules from the tiddlers we've just loaded
- $tw.wiki.defineTiddlerModules();
- // And any modules within plugins
+ // Register typed modules from the tiddlers we've just loaded and any modules within plugins
+ // Tiddlers should appear last so that they may overwrite shadows during module registration
$tw.wiki.defineShadowModules();
+ $tw.wiki.defineTiddlerModules();
// Make sure the crypto state tiddler is up to date
if($tw.crypto) {
$tw.crypto.updateCryptoStateTiddler();
@@ -2487,7 +2577,7 @@ $tw.boot.execStartup = function(options){
$tw.boot.disabledStartupModules = $tw.boot.disabledStartupModules || [];
// Repeatedly execute the next eligible task
$tw.boot.executeNextStartupTask(options.callback);
-}
+};
/*
Startup TiddlyWiki
*/
@@ -2506,7 +2596,7 @@ $tw.addUnloadTask = function(task) {
if($tw.unloadTasks.indexOf(task) === -1) {
$tw.unloadTasks.push(task);
}
-}
+};
/*
Execute the remaining eligible startup tasks
@@ -2539,11 +2629,13 @@ $tw.boot.executeNextStartupTask = function(callback) {
$tw.boot.log(s.join(" "));
// Execute task
if(!$tw.utils.hop(task,"synchronous") || task.synchronous) {
- task.startup();
- if(task.name) {
- $tw.boot.executedStartupModules[task.name] = true;
+ const thenable = task.startup();
+ if(thenable && typeof thenable.then === "function"){
+ thenable.then(asyncTaskCallback);
+ return true;
+ } else {
+ return asyncTaskCallback();
}
- return $tw.boot.executeNextStartupTask(callback);
} else {
task.startup(asyncTaskCallback);
return true;
@@ -2551,7 +2643,7 @@ $tw.boot.executeNextStartupTask = function(callback) {
}
taskIndex++;
}
- if(typeof callback === 'function') {
+ if(typeof callback === "function") {
callback();
}
return false;
@@ -2565,14 +2657,14 @@ $tw.boot.doesTaskMatchPlatform = function(taskModule) {
var platforms = taskModule.platforms;
if(platforms) {
for(var t=0; tThis community exists because TiddlyWiki is more useful when people share and work together.
This community is a beautiful but fragile thing: a collection of diverse people from all over the planet, united in their interest in the project, and their commitment to helping one another achieve and learn more.
We try to make the community as broad and welcoming as possible by remembering some basic principles of culture and behaviour.
These principles guide technical and non-technical decisions, and help contributors and leaders support our project and community.
We are optimistic and hopeful
We aim to foster a learning environment that is collaborative and safe for everyone
We recognise that the motivation for sharing and helping is usually for appreciation, and not financial gain, and so we take care to acknowledge and thank the people who enrich the community by sharing what they have created
While we are united in our interest in TiddlyWiki, we differ in every other conceivable way. We choose to focus on what unites us, and avoid unnecessarily mixing contentious topics like religion and politics
We treat each other with respect, and start with the assumption that others are acting in good faith
We avoid discriminatory language
We try to use our strength as a community to help others
We avoid responding when angry or upset because we try to de-escalate conflict
We make sure we critique ideas, not people
When we disagree with others we do so graciously, and treat others with dignity and respoect
We do not tolerate intolerance towards others
We seek first to understand others, and then to be understood
We have fun
Our discussions are in English. It is not the first language of many people in the community, nor do we all share the same cultural background and reference points. So we take care to use language that is clear and unambigous, and avoid cultural references or jokes that will not be widely understood.
It is not acceptable to make jokes or other comments that discriminate by race, gender, sexuality, or other protected characteristic.
As an inclusive community, we are committed to making sure that TiddlyWiki is an accessible tool that understands the needs of people with disabilities.
\ No newline at end of file
diff --git a/community/docs/Community Cards Caveats.tid b/community/docs/Community Cards Caveats.tid
new file mode 100644
index 000000000..c63a29d63
--- /dev/null
+++ b/community/docs/Community Cards Caveats.tid
@@ -0,0 +1,5 @@
+title: Community Cards Caveats
+created: 20250909171928024
+modified: 20250909171928024
+
+''Please note that [[Community Cards]] are a new initiative started in September 2025. There is further work required to complete the team and people information.''
diff --git a/community/docs/Community Cards.tid b/community/docs/Community Cards.tid
new file mode 100644
index 000000000..087eaac65
--- /dev/null
+++ b/community/docs/Community Cards.tid
@@ -0,0 +1,11 @@
+title: Community Cards
+tags: Community
+modified: 20250909171928024
+created: 20250909171928024
+
+The purpose of Community Cards is to allow project plans and other community activities to be linked to the people who are involved in them. They also allow people to share their interests and activities in the TiddlyWiki community, and to help people in the TiddlyWiki community get to know each other better.
+
+{{Community Cards Caveats}}
+
+* [[Submitting a Community Card]]
+* [[Displaying Community Cards]]
diff --git a/community/docs/Displaying Community Cards.tid b/community/docs/Displaying Community Cards.tid
new file mode 100644
index 000000000..3d371ccd8
--- /dev/null
+++ b/community/docs/Displaying Community Cards.tid
@@ -0,0 +1,26 @@
+title: Displaying Community Cards
+tags: [[Community Cards]]
+modified: 20250909171928024
+created: 20250909171928024
+
+!! Cards for people
+
+This is an inline card for <> and <> which can be used in the middle of a sentence.
+
+This is a stack of inline cards:
+
+<>
+
+Here is a full format card:
+
+<>
+
+This is how the card looks when there is no such person:
+
+<>
+
+!! Cards for teams
+
+This is a card for a project team:
+
+<>
\ No newline at end of file
diff --git a/community/docs/Submitting a Community Card.tid b/community/docs/Submitting a Community Card.tid
new file mode 100644
index 000000000..195bf289f
--- /dev/null
+++ b/community/docs/Submitting a Community Card.tid
@@ -0,0 +1,36 @@
+title: Submitting a Community Card
+tags: [[Community Cards]]
+modified: 20250909171928024
+created: 20250909171928024
+
+Anyone associated with the TiddlyWiki community can submit a Community Card. The submission process currently involves making a GitHub pull request but we intend to provide a more user-friendly submission process in the future.
+
+Pull requests to add or update a community card should be made against the `tiddlywiki-com` branch of the [[TiddlyWiki repository|https://github.com/TiddlyWiki/TiddlyWiki5]] in the directory `community/people`.
+
+The card should be a TiddlyWiki tiddler with the following fields:
+
+|!Field |!Required|!Description |
+|`title`|Yes |The username of the person represented by the card, starting with `@` (e.g. `@Jermolene`). This is the title of the card and should be unique |
+|`tags`|Yes |The tags for the card, including `Community/Person` |
+|`fullname`|Yes |The full name of the person or group represented by the card |
+|`avatar`|Yes |The base64 representation of the 32x32 avatar image for the person represented by the card |
+|`first-sighting`|No |The date of the first sighting in the community of the person represented by the card. This should be in ISO 8601 format (YYYY-MM-DD) |
+|`talk.tiddlywiki.org`|Yes |The username of the person or group on the TiddlyWiki Talk forum |
+|`github`|No |The username of the person or group on GitHub |
+|`linkedin`|No |The URL of the LinkedIn profile for the person or group represented by the card |
+|`flickr`|No |The URL of the Flickr profile for the person or group represented by the card |
+|`homepage`|No |The URL of the homepage for the person or group represented by the card |
+|`email`|No |The email address of the person or group represented by the card |
+|`text`|Yes |The text of the card. This should include a brief description of the person or group represented by the card, and any other relevant information |
+
+! Rules for Community Cards
+
+Community cards must observe the following rules. It is intended to enforce them with an automated script, but for the moment they will be manually checked.
+
+* `title` must be unique and start with `@`
+* `tags` must include `Community/Person`
+* `fullname` must be provided
+* `avatar` must be a base64 representation of a 32x32 image, with a limit of 1KB. [[Squoosh|https://squoosh.app/]] is recommended for resizing and compressing images
+* `first-sighting` should be in ISO 8601 format (YYYY-MM-DD)
+* `talk.tiddlywiki.org` must be provided
+* `text` total size must not exceed 2KB
diff --git a/community/people/Arlen22.tid b/community/people/Arlen22.tid
new file mode 100644
index 000000000..5bc102312
--- /dev/null
+++ b/community/people/Arlen22.tid
@@ -0,0 +1,10 @@
+title: @Arlen22
+tags: Community/Person
+fullname: Arlen Beiler
+first-sighting: 2011-06-20
+talk.tiddlywiki.org: arlen22
+github: Arlen22
+homepage: arlen22.github.io
+avatar: /9j/4AAQSkZJRgABAQAAAQABAAD/2wEEEAAVABUAFQAVABYAFQAYABoAGgAYACEAIwAfACMAIQAwAC0AKQApAC0AMABJADQAOAA0ADgANABJAG8ARQBRAEUARQBRAEUAbwBiAHcAYQBaAGEAdwBiALEAiwB7AHsAiwCxAMwArACiAKwAzAD4AN0A3QD4ATgBKAE4AZcBlwIkEQAVABUAFQAVABYAFQAYABoAGgAYACEAIwAfACMAIQAwAC0AKQApAC0AMABJADQAOAA0ADgANABJAG8ARQBRAEUARQBRAEUAbwBiAHcAYQBaAGEAdwBiALEAiwB7AHsAiwCxAMwArACiAKwAzAD4AN0A3QD4ATgBKAE4AZcBlwIk/8IAEQgAQABAAwEiAAIRAQMRAf/EADAAAAIDAQEAAAAAAAAAAAAAAAMFAQQGAgABAQEBAQEAAAAAAAAAAAAAAAIDAQAE/9oADAMBAAIQAxAAAADIRMd3XctQlXtCTTmB6RFvANDouy4DYwEEar6YVM7ocz57mcqnZys+V2azZU4XZSoiZqhQt9TKOlnO+GOl1HyoUPXLn//EACYQAAICAQQCAgEFAAAAAAAAAAECABEDBBIhMUFRECITFCMycZH/2gAIAQEAAT8AI4Bv4ryAeBAnANHuNidWogEwYHNRsdfA8iruVMOIu6iYtK4c714vgTDpXyOfrQHdifoArEXxM2mR0NeOhUzI+LJzYbuHszCm5hYseZh0gXYWFIai4cWJgFJuFKYvtr2sJRuB9fUzgDHlGMHia2757uYsYc0TNHpsSmzzMONjl9iu74iK6PbWT7gv/RMiZDk+qcA3NXkAVl3gE+ADU1PDVdiaDCGJZjQEyowKANS1ZMwK+HJ+3a0KUDqYnYINxJ3eItDk81M2cZD+NVIrmanU/wAl2gCZiGNiaFziJ3LYIHcXMrLvDABe17EN1vCgqR2TNPnGTBSBbDTeV3c2amdlxPuD2C3H9epqmV628xqsUYmdiuwkVVTSZ0Q/dxwYdScrgBRsqONi2KQX7mo1G4WCK20B6j6p/VpcfMXPVQ9mbhx9eLgZrFGDUZB1DqMrCma4xN8mDcR5qK5Rgw7Hx//EABwRAQEBAQACAwAAAAAAAAAAAAECABEDIhIxQf/aAAgBAgEBPwDVQYpfzd66qDeOSn7yEmH23ffDAi66mug6DM9N8HTAY3//xAAcEQEBAQEAAgMAAAAAAAAAAAABAAIREBIiQVH/2gAIAQMBAT8AglC+rJbdCT1vVC33l83tj2OPLS+AJ3+Tf//Z
+
+I make random software.
\ No newline at end of file
diff --git a/community/people/ChristianByron.tid b/community/people/ChristianByron.tid
new file mode 100644
index 000000000..5d8ffb7be
--- /dev/null
+++ b/community/people/ChristianByron.tid
@@ -0,0 +1,14 @@
+title: @Christian_Byron
+tags: Community/Person
+fullname: Christian Byron
+talk.tiddlywiki.org: Christian_Byron
+github: ceebeetree
+linkedin: www.linkedin.com/in/christian-byron-b84a594/
+avatar: /9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAgICAgJCAkKCgkNDgwODRMREBARExwUFhQWFBwrGx8bGx8bKyYuJSMlLiZENS8vNUROQj5CTl9VVV93cXecnNEBCAgICAkICQoKCQ0ODA4NExEQEBETHBQWFBYUHCsbHxsbHxsrJi4lIyUuJkQ1Ly81RE5CPkJOX1VVX3dxd5yc0f/CABEIACAAIAMBIgACEQEDEQH/xAAuAAEBAAMBAAAAAAAAAAAAAAAHBgEDBQQBAAMBAAAAAAAAAAAAAAAAAAABAwX/2gAMAwEAAhADEAAAADv2xtJlY03sqePW3ARS1RSydIhcH//EACcQAAICAgIBAgYDAAAAAAAAAAECAwQFEQASMRMhBhBBk8HRIzJx/9oACAEBAAE/AMFQxs+NExqJLMCwYE+SOT4bF3qr+hAIpRsDQ6lWH0Yco4S/eVniRVQHXZzrZ5dwGQpQtNII2RfJVvHMRl5cbKxC94n/ALp+RxfiKpNcgMMUqPIwjcnWip/I5XtUowaL3Ujir/xt79Glb6/4OZ7MV5oEpUzuIa7MPB14A5jpoYLsEsydo1bbLre+CWEEEYab7Uf74ZYSSThpvtR/vmRmhnuzywp1jZtquta+VPM49qlcy24lf017At7g8uZnHrUsGK3Ez+m3UBvcnXy//8QAHhEAAgEFAAMAAAAAAAAAAAAAAQIDAAQRIkEyUaH/2gAIAQIBAT8AmiuVlZkLEeQOflJPcvMAF0z65V+h0YIW52rBDuxUrztf/8QAIxEBAAEDAwMFAAAAAAAAAAAAAgEAAxEEBSMSQcEiMVJxof/aAAgBAwEBPwC/Z1ZvNBOYz1Gc/lDUat3ySPRM/H2P3W4hcbIldpxnxW3BcjQk9oznzX//2Q==
+
+Hello ~TiddlyWikiers - I have been a long time fan, recent contributor to the TW community.
+Recently I have volunteered to run the [[TiddlyWiki Newsletter|https://tiddlywiki.substack.com/]] to spread the great news about TW.
+
+I have been in the IT industry for about thirty years, mostly as a consultant and technical arcitect.
+More recently I went back to study a masters in IT focussing on AI and data science.
+Now my partner and I have started our own business ([[Sphere Innovations|https://sphere-innovations.com.au]]) - in consulting and building web applications for small to medium size businesses here in Australia.
diff --git a/community/people/EricShulman.tid b/community/people/EricShulman.tid
new file mode 100644
index 000000000..24201765f
--- /dev/null
+++ b/community/people/EricShulman.tid
@@ -0,0 +1,29 @@
+title: @ericshulman
+tags: Community/Person Community/Team/Contributors
+fullname: Eric Shulman
+first-sighting: 2005-06-21
+talk.tiddlywiki.org: ericshulman
+github: ericshulman
+homepage: tiddlytools.com
+email: elsdesign@gmail.com
+avatar: iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAD/ElEQVR42o2Tf2iUdRzH37e7rOa222233bab3mqKU9QihCAi+isKwX/sh5UQhGYQhNAvQowRUoghQWDOIJtQmOY0M92ZmVGm0WbTyZI1Nnft99S1jc3dPT9efffg/bHdLn19Hp4HPjzv9/fz+fL5aE58PwUkjzzFVC4P/G/k6E445Pc+uceeaqnv7Ogd6Rq68PPhrc+vkiERWOLT/+Ib8uQHNiXax3BIM0mC+CEtl2G7X9mIeCV+9Ejrr2MAtgkH14SNBRZXrYYPNF86nsXCkx/8dATAsp0JhknQTYJrTHg5SNI0qMekb+aw8Hr74WCKpNNu/0Kck5ymkRMcZz/1Jv5g2CUFbZYelrbMvlBMonHvJK3JuPsdTQxwExc8XG7SxF7OcxGScP6wRGCG/Asjf39VPydTzbQyRBrXBKToBCP/nQQ9VpIDO6SumU3EjUFLzX766HMG0mIvoJnXEbU47GGXc4TGBs3zWp5Jh7F47omdf56hy9lLIz3gyYfZSQMJztFEH3KEDg+bf1dkzkO9Savks7H9NLqnuEw3MEU314nTwABj/MV2R6y8JL+0wKdM8MtX23aFy04dF5mg08QI6XYsemmzRfiMDP5Mg1emK4ienZxi0p0gBfRwhSHAxgXGGeS6tYUdu6TPA3Ofr3Mfj9Bv4zHMDaCTMcBlnG4cJqx64sagN9Ngw3RJoa5R+MftI8k1Wm7NcSsH6KKPFGBbG1n1srQ+06DWpJ59cRhsGKGbo0wBFpDgNGcBcHGsl9BuSZmjfCRHWnv0BtgOcJVWwAZG2cw+3uErAKacZ6hq32PkGWuNSaxsHgIHxqjje5I4/Ms2dCt+BHpcUT4ai0j5sw22TCea2sCBbz3BOjaRFj+JeAE46IoHxlUmlfrmWuZT+8Ae935fjljDe3zpLdEJxGriLHdFtL8mKC2cbbAgIOXVBemwBhHibZq4xN/0YgPrESsRsiMs+C1zEwwFxqBqs4hY2yhlKeIUab5GLEM8SLlVRslu77jZhEwL/ofKKZ4uknxiiLO0cYFGFpJPMTGiRO0iQqtNrX7NxueTcahqv4/FTpgwFYinOcoxtiLKWEwF+U6Mqv5FuVlWSQHzvBWmKmUqIEg1YiMfIu6lhjKCRK0YkXelwoDmIjztWrCot5KQs5R5zKccIVZQwl3cTaVdQVGnfOkrzFbDuvuJWTVuBcXcQ5iFlFFAmBynlBKKH/f6z06pX6r6pJoSQlaeW2gsighi3na1E6HwNSkUUHbS45FXG7ajhIi68+1cO98qtqqJEHzTW6LbEfUstER1ef2llBKhiGqKW7VGUk6lT7dnmS/gnZMf1KPaoI16VWsrA1KhX3dObo5m9VqQpff/AFTcI4hMzFV+AAAAAElFTkSuQmCC
+
+\define wiki(text,topic) [[$text$|https://en.wikipedia.org/wiki/$topic$]]
+
+''Hello! My name is Eric Shulman''. I am the author of ''[[www.TiddlyTools.com|http://www.TiddlyTools.com]] (Small Tools for Big Ideas! ™)'', a popular collection of original plugins, macros, widgets, templates and stylesheets for TiddlyWiki that I have created and shared with the TiddlyWiki community.
+
+<<<
+Think of TiddlyTools as a ''virtual hardware store and "demonstration showroom"'', offering tools, parts and techniques that provide a rich variety of new functionality and feature enhancements to help you ''turn a general-purpose TiddlyWiki "info-house" into a comfortable, custom-built "info-home"''.
+
+The TiddlyWiki core system provides the basic structure and utilities: the foundation, framing, walls, roof, windows/doors, plumbing, heating, and electrical systems. Then, TiddlyTools helps you with all the "finish work": the appliances, fixtures, lighting, cabinets, furniture, paint, wallpaper, carpeting, etc. ''to best suit your specific needs and personal style''.
+<<<
+
+Since the early days of TiddlyWiki (April 2005), I have worked closely with its inventor, [[Jeremy Ruston|https://jermolene.com/]], to help develop and improve TiddlyWiki's core functions. I am also a key contributor and administrator of the online TiddlyWiki [[Discourse|https://talk.TiddlyWiki.org]] and [[GoogleGroups|https://groups.google.com/forum/#!forum/tiddlywiki]] discussion forums, providing ongoing assistance to the worldwide TiddlyWiki community. I have written over 15,000 detailed responses to individual questions posted online. For several years I was also the lead developer and maintainer of the [[TiddlyWiki Classic|https://classic.tiddlywiki.com/]] codebase.
+
+I was born and raised in suburban Long Island, NY, and attended [[Carnegie Mellon University (CMU)|https://www.cmu.edu/]] in Pittsburgh, PA, where I studied ''Computer Science, Cognitive Psychology, Sociology, Human Factors Design, and Artificial Intelligence''. As an undergraduate at CMU, I was privileged to work with some of the major luminaries in early software research and design, including <>, <>, <>, and <>. I was also employed in several Computer Science Department research projects, including the development of speech recognition technologies, graphical interface systems, and interactive applications for instruction in physics, art and music. I received a ''Bachelor of Science in "Interactive Systems Design"'' from CMU in 1985.
+
+During my early post-graduate years, I worked for several notable software development companies, including
+<> and <>. I was an integral member of the <> development team where I helped create the first GUI-based application interfaces for Microsoft Windows and IBM OS/2.
+
+Since 1998, I have been an ''independent design consultant'', living and working in Silicon Valley, where I apply more than 40 years of experience to provide ''analysis, design and software development services'' for commercial companies and not-for-profit organizations, with emphasis on ''information architecture'' and ''interaction/visual design standards'' to improve ease-of-use for new and existing software products and online environments.
diff --git a/community/people/Jermolene.tid b/community/people/Jermolene.tid
new file mode 100644
index 000000000..b75e410c7
--- /dev/null
+++ b/community/people/Jermolene.tid
@@ -0,0 +1,21 @@
+title: @Jermolene
+tags: Community/Person
+fullname: Jeremy Ruston
+first-sighting: 2004-09-20
+talk.tiddlywiki.org: jeremyruston
+github: Jermolene
+linkedin: www.linkedin.com/in/jermy
+flickr: www.flickr.com/photos/jermy/
+bluesky: https://bsky.app/profile/jermolene.bsky.social
+homepage: jermolene.com
+email: jeremy@jermolene.com
+avatar: /9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAgICAgJCAkKCgkNDgwODRMREBARExwUFhQWFBwrGx8bGx8bKyYuJSMlLiZENS8vNUROQj5CTl9VVV93cXecnNEBCAgICAkICQoKCQ0ODA4NExEQEBETHBQWFBYUHCsbHxsbHxsrJi4lIyUuJkQ1Ly81RE5CPkJOX1VVX3dxd5yc0f/CABEIACAAIAMBIgACEQEDEQH/xAAtAAEBAAMAAAAAAAAAAAAAAAAHBgIEBQEBAQEBAAAAAAAAAAAAAAAAAgQBBf/aAAwDAQACEAMQAAAANF4uTuPRhD2nBLnUiJvKM0DtMKy//8QAKxAAAgIBAwMDAQkAAAAAAAAAAQIDBBEABRITITEiMkFxFEJRUmFicoGR/9oACAEBAAE/AInTA6gUGP4ZOQbW1bPsmyUq1q+gmvFPUzZPDkPamtwqU75ks04JakroVcg5RwRjg66NUx25KbzqJYyMngfqSuq0M3NZYIebJIvZozIvI/iNPcp/aalSdJXsS4VcKeIzlvU3jVTcYLNiaGISrjkhWQYDfQ63pYAzCDBsOiu7Dsx4EHH6r2w2ttimjd2IsNErhhJHKI04/uzqxuCxpBYVVWKSHqwMyMSQ33SB7dUJFmlkMYRgnqZgCMf7rf8AeEt3A9YOhjXAb2k8u7dtT1RZeOtXmYxiOPj4ZWY/lb51skqUNnNW/wBNzC7IpB6gQeeB/jq/fqGOaLbowuYn5MAQOw8LjW5Vmeo0qIsqYLLKjHIZmwv9fB1//8QAHxEAAQMEAwEAAAAAAAAAAAAAEQABAgMSIWExMkFR/9oACAECAQE/AD9iTy2lJmHUB8BVKM4SNSOj46a29saX/8QAHREAAgICAwEAAAAAAAAAAAAAAQIAAwQRITGBkf/aAAgBAwEBPwDHpFpJZtamVSiBWT2Yt7hmCDsb+TKtsKqpGg3M/9k=
+
+I'm the original inventor of TiddlyWiki. You can hire me through my consultancy company [[Intertwingled Innovations|https://intertwingledinnovations.com]] or contact me directly.
+
+Further information:
+
+* A recording of the [[keynote I gave at QCon London in April 2024|https://www.infoq.com/presentations/bbc-micro/]], and the [[discussion on talk.tiddlywiki.org|https://talk.tiddlywiki.org/t/recording-of-jeremys-keynote-at-qcon-london-april-2024/10505]]. The talk mixes some nostalgia about my teenage activities with the BBC Micro with thoughts on the development of the software industry and insights gained from working with TiddlyWiki
+* An [[interview with me in The Inquirer|https://web.archive.org/web/20111103225832/http://www.theinquirer.net/inquirer/feature/2105529/bt-software-engineer-tells-telco-source]] by Wendy Grossman
+* A [[hilarious interview with me|https://www.youtube.com/watch?v=auyIhw8MTmQ]] from British television in 1983
+* Here's a video of a presentation I did in 2007 called [["How to Start an Open Source Project"|http://vimeo.com/856110]].
diff --git a/community/people/LinOnetwo.tid b/community/people/LinOnetwo.tid
new file mode 100644
index 000000000..02b4c46e2
--- /dev/null
+++ b/community/people/LinOnetwo.tid
@@ -0,0 +1,19 @@
+avatar: /9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAgICAgJCAkKCgkNDgwODRMREBARExwUFhQWFBwrGx8bGx8bKyYuJSMlLiZENS8vNUROQj5CTl9VVV93cXecnNEBCAgICAkICQoKCQ0ODA4NExEQEBETHBQWFBYUHCsbHxsbHxsrJi4lIyUuJkQ1Ly81RE5CPkJOX1VVX3dxd5yc0f/CABEIACAAIAMBIgACEQEDEQH/xAAuAAEAAwEBAAAAAAAAAAAAAAAGAwQHAgUBAAMBAAAAAAAAAAAAAAAAAAACAwT/2gAMAwEAAhADEAAAAOfCWAMdKKetM4wOvY5OcvZnrYf/xAApEAACAQQBBAECBwAAAAAAAAABAgMABAURQQYSIVETFCIxMkJicYKh/9oACAEBAAE/AEtysaStr7mPaPeuazWdMM4gEnfPryW8hBUuZvou2RXRxyreBWPmgyNqs8f8MOQalhdY7Vz+R4/s/qfP+1edNi/zl7HDcFbmS3E8CcMR4INP0PkBhklIm+sZNtFtQiV0nj57Owl+dSrSTFgD6/CtH4VV9lU3oAbPngAVY389lc5URuUZkMxhnR4pvW0VwDqsP1FNmLWYqCpikMbngmliJNY+aKzyTxXS6lRAyg/u5rq+5x2RsuyTa3MQMlvKniRGThTUd1JYXUdzAwDvqVxGdRXMbfrVOD7HBrG3mNEsU8z98TRhl9eRzX//xAAcEQACAgIDAAAAAAAAAAAAAAABAgARAzESIVH/2gAIAQIBAT8ARuXZPsul3Eoje5lBQWBP/8QAGREAAwEBAQAAAAAAAAAAAAAAAAECEiER/9oACAEDAQE/AM98Lk7LJe20z//Z
+created: 20251110102157310
+first-sighting: 2019-03-01
+fullname: Lin Onetwo
+github: linonetwo
+homepage: https://wiki.onetwo.website/
+modified: 20251111184556193
+tags: Community/Person Community/Team/Contributors
+talk.tiddlywiki.org: linonetwo
+title: @linonetwo
+type: text/vnd.tiddlywiki
+
+Since 2014, when I started college, I've been on a quest for a lifelong PKM tool. I cherish my life and all my experiences, and I don’t want to forget any of them. When I’m deeply focused on a task, it’s easy to lose sight of other important parts of my life—so I needed a system to help me stay balanced.
+
+Early on, I tried TiddlyWiki several times, but I was initially put off by its save mechanism and markup editing. That changed when I discovered an auto-backup script, which gave me the confidence to fully commit. Over time, I improved the script and eventually transitioned to using TidGi-Desktop and TidGi-Mobile.
+
+Today, my TiddlyWiki holds all my game design ideas and progress logs—it has truly become my second brain. With the help of LLM-powered programming tools, I’ve enhanced it with numerous plugins, allowing me to manage my mind in a more programmable and structured way. As a game developer, TiddlyWiki isn't the core of my professional work; But I've invested so much time because it's fundamentally about upgrading my mind.
+
+Most of my notes are open by default and shared publicly on my homepage as a digital garden.
diff --git a/community/people/MotovunJack.tid b/community/people/MotovunJack.tid
new file mode 100644
index 000000000..0d8bee7ae
--- /dev/null
+++ b/community/people/MotovunJack.tid
@@ -0,0 +1,11 @@
+title: @MotovunJack
+tags: Community/Person Community/Robot
+fullname: Motovun Jack
+first-sighting: 2012-01-12
+github: MotovunJack
+homepage: tiddlywiki.com
+avatar: /9j/4AAQSkZJRgABAQAAAQABAAD/2wEEEAAYABgAGAAYABkAGAAaAB0AHQAaACUAKAAjACgAJQA2ADIALgAuADIANgBSADsAPwA7AD8AOwBSAH0ATgBbAE4ATgBbAE4AfQBuAIYAbQBlAG0AhgBuAMYAnACKAIoAnADGAOUAwQC2AMEA5QEWAPgA+AEWAV4BTAFeAckByQJmEQAYABgAGAAYABkAGAAaAB0AHQAaACUAKAAjACgAJQA2ADIALgAuADIANgBSADsAPwA7AD8AOwBSAH0ATgBbAE4ATgBbAE4AfQBuAIYAbQBlAG0AhgBuAMYAnACKAIoAnADGAOUAwQC2AMEA5QEWAPgA+AEWAV4BTAFeAckByQJm/8IAEQgAQABAAwEiAAIRAQMRAf/EADAAAAIDAQEAAAAAAAAAAAAAAAMEAQIFBgABAQEBAQEAAAAAAAAAAAAAAAIDAQAE/9oADAMBAAIQAxAAAADZCfn5vZJz+rnODGtpbpm6O8xzG9lCiszXtikQhtkTBputBxURJuVVYlEdBaQ284mPDj6GmkNUblMxRmi7dKw//8QAKxAAAgIBAgUCBgMBAAAAAAAAAQIAAxESIQQTIkFRFGEjMUJxgaEyNGLR/9oACAEBAAE/AMmX3ilMkjPaV3ragZDtNRmoxpvA2sEqQcHEwJxlwa98nYbCU8TymDfSTvPVKMbZHkQcTU4yDH46tTiE8RxjLXnQp7Dx5MACgKuyqMAS1xXU7kjYTiEbWp3y0IucYbGx6e05hDAqMH/k59o3DfxAE5hss1MNzODdVraxu50ieppH1Tivi8O6eYQ1j4B6guAftChDMNjBqycHcCYJqdj2s3idRBHfpi/1Kie7PDo95w/EMxYM22n9yy5AzBc/iLe7dIqx7kyy2ypyOWoYTofhCyDAZtx4MOmpK9sncyx1NdSq2kBBt3EKf6mgIzDUPIiByuqk7faMLbOyEjuuxEAyo56AgeTA3KL1AYRm1CcvmkgAs2wHjEvPxGIMJPmHUQCQNothr32A0ggeYluplcAK2PlLbTytZUkdwI7V3lAQMgbAfP8AMoCV1AKMOR+pdsc5yD595mMmNIGD4h0vsfupHyBlTKW9znMd+TQnljPWqHYIqhwD1zKsqtjBzCAVAyBicnqG6jbOe0//xAAbEQEBAAMBAQEAAAAAAAAAAAABAAIRIRASQf/aAAgBAgEBPwBYbZDuXvnLE5OrkWJzxI4g33ift//EABsRAAMBAQADAAAAAAAAAAAAAAABEQIhEBJB/9oACAEDAQE/AMqjzHwjGoZXPHTb6Zp1/TRp1khYjW01xHqz/9k=
+
+Motovun Jack is a robot that helps maintain the TiddlyWiki project infrastructure. It is not a person, but rather a set of automated scripts and tools that assist in managing the various services and resources used by the TiddlyWiki community.
+
+The origin of the name "Motovun Jack" is a lovable and playful kitten encountered by [[@Jermolene]] in the beautiful medieval hill town of Motovun in Croatia. Jack was [[first adopted|https://github.com/TiddlyWiki/TiddlyWiki5/commit/ecfbaaa5641f14e1766ef17ef6416bf9aa992863]] as the TiddlyWiki 5 mascot in 2012.
diff --git a/community/people/PMario.tid b/community/people/PMario.tid
new file mode 100644
index 000000000..a91259935
--- /dev/null
+++ b/community/people/PMario.tid
@@ -0,0 +1,25 @@
+avatar: UklGRiwIAABXRUJQVlA4WAoAAAAwAAAAPwAAPwAASUNDUCACAAAAAAIgbGNtcwRAAABtbnRyR1JBWVhZWiAH6QALAAoACwADAAZhY3NwTVNGVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWxjbXMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZkZXNjAAAAzAAAAG5jcHJ0AAABPAAAADZ3dHB0AAABdAAAABRrVFJDAAABiAAAACBkbW5kAAABqAAAACRkbWRkAAABzAAAAFJtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFIAAAAcAEcASQBNAFAAIABiAHUAaQBsAHQALQBpAG4AIABEADYANQAgAEcAcgBhAHkAcwBjAGEAbABlACAAdwBpAHQAaAAgAHMAUgBHAEIAIABUAFIAQwAAbWx1YwAAAAAAAAABAAAADGVuVVMAAAAaAAAAHABQAHUAYgBsAGkAYwAgAEQAbwBtAGEAaQBuAABYWVogAAAAAAAA81EAAQAAAAEWzHBhcmEAAAAAAAMAAAACZmYAAPKnAAANWQAAE9AAAApbbWx1YwAAAAAAAAABAAAADGVuVVMAAAAIAAAAHABHAEkATQBQbWx1YwAAAAAAAAABAAAADGVuVVMAAAA2AAAAHABEADYANQAgAEcAcgBhAHkAcwBjAGEAbABlACAAdwBpAHQAaAAgAHMAUgBHAEIAIABUAFIAQwAAVlA4TOYFAAAvP8APEDWGgbRtWv+yt/0WImICOBvWn1C4dFi1bStbvpY8Qg2ePANNNAMh3N2db/7A91/7CHBvBBRr25ZFH+4k98ihkqi2CP4tsANvX8a+8y8Ct04dn0nuUt39ZiBJkowqt911M+MJ1G3bNiZJr1iP0DZ+2bbdadsqprOjAqmoUIX9hf3Fl5/uPYV7I3OMeoFzIvrvwG0kRUr3zPLdYMMXaqrMMsp0K4fufKO6c2hFV5Zh7kRROZX0PSCmB/3KWQwpuiekWelSRZDW94d0q750NrxavpFn1eLNQ9EV8nWlmAET6Q8lrCRTcjFLlLImluK3iXJW/hT47KGklS8OlzWUtXLFYDRCSS74ojUjxggqKMoxd6A1lTCyvsvyzC5/d7BsCHb7yIcHyrX2yR/NPnsAdRT2i0Pwp/o0Il6ix8hsRAuJmQgcr4KREfAiMgUVm9KqmfSxL5pOJspVwwTiV6jiIAg1RMhHpERhbvwgGI34Hc49T7UeKZtXwEqJ+BAaoBneperJH0POs1u4dufwv8Gf+qcOfjyvX6ZIVgxE0Rw87YF3BSc9c7jsXfdjOBG7FwmSb39pfGRwu8IuvUjJNoTpFzkEvDg6W3Qt/9nf99ZXPy8HM43IweTKyNR+WVatXcWWyakBksj9cqW+QetplcjsKElvZH/zuOO/PrCx//tL3/6x/O/C1PZZvSKuulLcS4l8M1ewGPR6ef5sllXW2eGQZ7hVSEZiPmcqrSS8e2ElX8o7t1fvB9LFetmEx5hx1Xuye2PpfjZnSjj7QfKTB3bZZo05Zvh6YuivX24cpc8+ddvADWG9odrSwFalVurxUiidDHmTiaoNkkh2gjbcpxMiAbd39aVP119/N9k4+euNKfcNjwaPhZEuUupUsJrHchw1LkPrRC9bQKa3M8Mj/xx903drdnHMpbirj1ENsUre0oo3N+7gat+2ZctKdsIUYc21sRu+Ucdhn+P7DyarftW00iu3Tmbv+hTfdCTmyaIPT4PrYZDFtBN2W8S9m4oTB5Z2P3Oe7weKjVBq86kXX/r0+WuvTAzfjqm1hsYRPWlbxm4n3IaeGOJEizv8orH9w5ejjmSrfOuEq/HxT6eDemtsZ/HTvvG1/8iVspxZILrlkz/cdsIbIroOgJileFSty2xiHNW5t9fbHJ3ze87bp5T9vc8RuqMB0ReDSt464R/BJxspvgpEsrVAJMTsYg2QovPTOHrvQ9et/S2Xx+40z7dY4JBX0Pz/ElH/T73U2DkK8EiqC9hM/zV3frQfzjaAqO16s1l6xCUXnBFlYxyIer3eEdth7u5xsHKxWoGLqzY3wIULt9G3K6soei9jZ+UcF+Ka3M/II9EUWrJ/LLxy+Q9xIh0vOl3NZCrVnBsuFUTOSnJnSioRWZ9q4g+ZDk5XVORoW2qX2hbIkna3JOrdR3jmpHVLovUkLES6grRO010u0GkDlX7SpH1DQ64Wl2zaSUJv1Mtti2G7kx5IyftWMhfDlGClcxvIUhP5crhp9LIb1Vne187oSAWxelcR/kXjYQTZboW+Oj1pqF0gmfZhSDD6bSgzGWrw3s7QLNtCV+2uatYrd/aFtjDI8R52e/DdyKgRKXBhEak3Ev50+GCUA9EFUor39htVMxmWvW8AM6ptG416rZvdWn+MarIEyH5r6ruZSrx8XrWDP370vbfTjqpmZGIbiFPFoihc4jcrlYi9p3ndSuymZ+XLaKza/P/HUWHn5Axdkd9OjBskY0+pIlz4AlFPFs+aStK5PBIRR4MVVJDihsy4JdEA4pVcrVqMZDyL2/8aYocikEAR9Xjc1BNG9zEiJG7n/cGyrtnblkClBhEgMW4Kx21BEBGJjLa0hcOGmTK64KsKLfKr9QyQELclxY3hqowTIZKdZNTSS5BWiBPlKxDWBVSS41bOepkhTkhGDajLfLyUBOKlkMHPgOhx3JoRN/cEiRgSWdgF2yCyDQu4IcbNo8ftTzxveOJ5y+h509h52+h549h569h587/M20f/b1AB
+created: 20251110102157310
+first-sighting: 2009-11-14
+fullname: Mario Pietsch
+github: pmario
+homepage: https://wikilabs.github.io/
+modified: 20251110124935183
+tags: Community/Person Community/Team/Contributors
+talk.tiddlywiki.org: pmario
+title: @pmario
+type: text/vnd.tiddlywiki
+youtube: https://www.youtube.com/@pmario
+
+''Hi, My name is Mario Pietsch''. Back in 2009 I was ''searching'' for ''a simple presentation tool'' and discovered ~TiddlyWiki Classic, Monkey Pirate ~TiddlyWiki ([[MPTW|https://mptw.tiddlyspot.com/]]) with ~TagglyTagging, Eric Shulman's ~TiddlyTools, Saq Imtiaz's navigation macros, and more. --- ''I was captivated''.
+
+After a deep dive, I combined these elements into my own "Presentation Manager", along [[3 step by step tutorials|https://groups.google.com/g/tiddlywiki/c/qG_tZ1x0MEU/m/-vLA0luMicYJ]] to help others build it.
+
+Thanks to ''the positive spirit'' of the ~TiddlyWiki community, I am proud to be part of it since 2009.
+
+When Jeremy started developing ~TiddlyWiki 5 on ~GitHub, I joined in—opening [[issue no. 1|https://github.com/TiddlyWiki/TiddlyWiki5/issues/1]] all the way up to 13. For what that’s good ;) Since then, I have submitted nearly 600 pull requests and more than 500 issues, many of which have been merged or resolved.
+
+My ~TiddlyWiki 5 "laboratory" is at https://wikilabs.github.io, and I also share content on my ''~YouTube'' channel: https://www.youtube.com/@pmario
+
+Have fun!
+Mario
diff --git a/community/project/TiddlyWiki People.tid b/community/project/TiddlyWiki People.tid
new file mode 100644
index 000000000..64ac944d8
--- /dev/null
+++ b/community/project/TiddlyWiki People.tid
@@ -0,0 +1,10 @@
+title: TiddlyWiki People
+modified: 20250909171928024
+created: 20250909171928024
+tags: Community About
+
+Members of the TiddlyWiki community who are involved in the development of TiddlyWiki and the running of the project are invited to [[create a Community Card|Submitting a Community Card]] so that they can be included in project plans and organisation charts. Community Cards can also showcase their interests and activities in the TiddlyWiki community.
+
+{{Community Cards Caveats}}
+
+<>
\ No newline at end of file
diff --git a/community/project/TiddlyWiki Project.tid b/community/project/TiddlyWiki Project.tid
new file mode 100644
index 000000000..b0d62cf1f
--- /dev/null
+++ b/community/project/TiddlyWiki Project.tid
@@ -0,0 +1,10 @@
+title: TiddlyWiki Project
+modified: 20250909171928024
+created: 20250909171928024
+tags: Community About
+
+The TiddlyWiki Project is the coordinated, ongoing effort to maintain and improve TiddlyWiki, and to support the TiddlyWiki community.
+
+{{Community Cards Caveats}}
+
+<$list filter="[tag[Community/Team]]" template="$:/tiddlywiki/community/cards/ViewTemplateBodyTemplateTeam"/>
\ No newline at end of file
diff --git a/community/project/Vacant Positions.tid b/community/project/Vacant Positions.tid
new file mode 100644
index 000000000..8408c793b
--- /dev/null
+++ b/community/project/Vacant Positions.tid
@@ -0,0 +1,4 @@
+title: Vacant Positions
+tags: [[TiddlyWiki Project]]
+
+If you are interested in volunteering to help the project please get in touch with <>.
\ No newline at end of file
diff --git a/community/project/teams/Core Team.tid b/community/project/teams/Core Team.tid
new file mode 100644
index 000000000..932477fc0
--- /dev/null
+++ b/community/project/teams/Core Team.tid
@@ -0,0 +1,8 @@
+title: Core Team
+tags: Community/Team
+modified: 20250909171928024
+created: 20250909171928024
+leader: @Jermolene
+team: @saqimtiaz
+
+The core team is responsible for the maintenance and development of the TiddlyWiki core and official plugins.
diff --git a/community/project/teams/Developer Experience Team.tid b/community/project/teams/Developer Experience Team.tid
new file mode 100644
index 000000000..cc6e28a62
--- /dev/null
+++ b/community/project/teams/Developer Experience Team.tid
@@ -0,0 +1,19 @@
+title: Developer Experience Team
+tags: Community/Team
+modified: 20251109200632671
+created: 20251109200632671
+leader: @pmario
+team: @saqimtiaz
+
+The Developer Experience Team improves the experience of software contributors to the TiddlyWiki project. This includes enhancing documentation, streamlining contribution processes, and providing tools and resources to help developers effectively contribute to TiddlyWiki.
+
+Tools and resources managed by the Developer Experience Team include:
+
+* Advising and assisting contributors, particularly new developers
+* Maintenance of developer-focused documentation on the https://tiddlywiki.com/dev/ site, including:
+** Development environment setup guides
+** Code review processes and best practices
+** Contribution guidelines and documentation
+* Continuous integration and deployment scripts providing feedback on pull requests
+* Devising and implementing labelling systems for issues and pull requests
+* Automation scripts to simplify common development tasks
diff --git a/community/project/teams/Infrastructure Team.tid b/community/project/teams/Infrastructure Team.tid
new file mode 100644
index 000000000..047c585a0
--- /dev/null
+++ b/community/project/teams/Infrastructure Team.tid
@@ -0,0 +1,15 @@
+created: 20250909171928024
+modified: 20251110133437795
+tags: Community/Team
+team: @MotovunJack
+title: Infrastructure Team
+
+The Infrastructure Team is responsible for maintaining and improving the infrastructure that supports the TiddlyWiki project. This includes the hosting, deployment, and management of the TiddlyWiki websites and services, as well as the tools and systems used by the TiddlyWiki community.
+
+The infrastructure includes:
+
+* talk.tiddlywiki.org
+* github.com/TiddlyWiki
+* tiddlywiki.com DNS
+* Netlify account for PR previews
+* edit.tiddlywiki.com
\ No newline at end of file
diff --git a/community/project/teams/MultiWikiServer Team.tid b/community/project/teams/MultiWikiServer Team.tid
new file mode 100644
index 000000000..922cf7582
--- /dev/null
+++ b/community/project/teams/MultiWikiServer Team.tid
@@ -0,0 +1,8 @@
+title: MultiWikiServer Team
+tags: Community/Team
+modified: 20250909171928024
+created: 20250909171928024
+leader: @Arlen22
+team:
+
+The MultiWikiServer development repository is at https://github.com/TiddlyWiki/MultiWikiServer
diff --git a/community/project/teams/Project Team.tid b/community/project/teams/Project Team.tid
new file mode 100644
index 000000000..5cd92cc3b
--- /dev/null
+++ b/community/project/teams/Project Team.tid
@@ -0,0 +1,15 @@
+title: Project Team
+tags: Community/Team
+modified: 20250909171928024
+created: 20250909171928024
+icon: $:/tiddlywiki/community/icons/project-team
+leader: @Jermolene
+team: @saqimtiaz @ericshulman
+
+The project team is responsible for the overall TiddlyWiki project, its vision, mission and values, and ensuring that it meets the needs of the community.
+
+Areas of responsibility include:
+
+* Communicating and demonstrating the vision, mission and values of the project
+* Continuously improve the development process and practices of the project
+* more to come...
diff --git a/community/project/teams/Quality Assurance Team.tid b/community/project/teams/Quality Assurance Team.tid
new file mode 100644
index 000000000..093a0ca15
--- /dev/null
+++ b/community/project/teams/Quality Assurance Team.tid
@@ -0,0 +1,11 @@
+title: Quality Assurance Team
+created: 20251112125742296
+modified: 20251112125742296
+tags: Community/Team
+team:
+leader: @Leilei332
+
+title: Quality Assurance Team
+
+The Quality Assurance Team is responsible for ensuring the quality and reliability of TiddlyWiki releases. This includes reviewing code submissions, testing new features, identifying bugs, and verifying that fixes are effective.
+
diff --git a/community/project/teams/Succession Team.tid b/community/project/teams/Succession Team.tid
new file mode 100644
index 000000000..e57a7affd
--- /dev/null
+++ b/community/project/teams/Succession Team.tid
@@ -0,0 +1,13 @@
+title: Succession Team
+tags: Community/Team
+modified: 20250909171928024
+created: 20250909171928024
+leader: @Jermolene
+team: @saqimtiaz @ericshulman
+
+The Succession Team is responsible for ensuring that personnel changes do not impact access to the external infrastructure used by the project.
+
+* Work with the other teams to ensure that the project has a succession plan for key personnel
+* Work with the other teams to ensure that they are using the appropriate, community-owned infrastructure
+* Ensure that the members of the succession team share ownership of the key project resources (eg passwords and user accounts). The Succession Team is not expected to use their access rights apart from managing access in the event of personnel changes
+
diff --git a/community/project/teams/TiddlyWiki Newsletter Team.tid b/community/project/teams/TiddlyWiki Newsletter Team.tid
new file mode 100644
index 000000000..229653f0a
--- /dev/null
+++ b/community/project/teams/TiddlyWiki Newsletter Team.tid
@@ -0,0 +1,15 @@
+title: TiddlyWiki Newsletter Team
+tags: Community/Team
+modified: 20251219090709874
+created: 20250909171928024
+leader: @Christian_Byron
+
+The Newsletter Team is responsible for producing the [[TiddlyWiki Newsletter]]. We would love to have your help if you would like to get involved.
+
+! Audience
+
+The newsletter is intended for TiddlyWiki end users who do not track all the discussions on https://talk.tiddlywiki.org/.
+
+Coverage of developer topics such as JavaScript and intricate wikitext should be handled thoughtfully to avoid alienating the core audience of end users.
+
+Subscribing to the newsletter is intended to give people confidence that they will not miss any important developments.
diff --git a/community/project/teams/tagCommunityTeam.tid b/community/project/teams/tagCommunityTeam.tid
new file mode 100644
index 000000000..426e98f6c
--- /dev/null
+++ b/community/project/teams/tagCommunityTeam.tid
@@ -0,0 +1,5 @@
+title: Community/Team
+modified: 20250909171928024
+created: 20250909171928024
+list: [[Project Team]] [[Core Team]] [[Documentation Team]] [[Quality Assurance Team]] [[Infrastructure Team]] [[MultiWikiServer Team]] [[Newsletter Team]] [[Succession Team]]
+
diff --git a/community/readme.md b/community/readme.md
new file mode 100644
index 000000000..09cd58c5b
--- /dev/null
+++ b/community/readme.md
@@ -0,0 +1,3 @@
+# Community Records and Resources
+
+These raw tiddlers comprise the community records and resources for the TiddlyWiki project. They are packaged as a root directory outside of the usual "editions" folder so that they can be shared with other wikis.
diff --git a/community/tools/cards/DefaultColourMappings.multids b/community/tools/cards/DefaultColourMappings.multids
new file mode 100644
index 000000000..6cab7fdc8
--- /dev/null
+++ b/community/tools/cards/DefaultColourMappings.multids
@@ -0,0 +1,15 @@
+title: $:/config/DefaultColourMappings/
+
+community-card-background: #ffffee
+community-card-foreground: #441111
+community-card-dark-shadow: rgba(188, 189, 189, 0.5)
+community-card-shadow: rgba(212, 212, 213, 0.5)
+community-card-header-background: #9e3060
+community-card-header-foreground: #ddddee
+community-card-team-header-background: #306090
+community-card-team-header-foreground: #ddeedd
+community-card-vacancy-header-background: #609030
+community-card-vacancy-header-foreground: #eedddd
+community-card-info-background: #f3f38b
+community-card-info-foreground: #444411
+community-card-field-name-foreground: #888844
diff --git a/community/tools/cards/Procedures.tid b/community/tools/cards/Procedures.tid
new file mode 100644
index 000000000..9d3879012
--- /dev/null
+++ b/community/tools/cards/Procedures.tid
@@ -0,0 +1,168 @@
+title: $:/tiddlywiki/community/cards/Procedures
+tags: $:/tags/Global
+
+\procedure community-card-display-jpeg-field(fieldName,mode:"block",default)
+<$genesis $type={{{ [match[block]then[div]else[span]] }}} class={{{ tc-community-card-field-image [[tc-community-card-field-image-]addsuffix] +[join[ ]] }}}>
+ <%if [has] %>
+ getaddprefix[data:image/jpeg;base64,]] }}} width="32"/>
+ <%else%>
+ <$transclude $tiddler=<> $mode=<>/>
+ <%endif%>
+$genesis>
+\end community-card-display-jpeg-field
+
+\procedure community-card-display-transclusion(fieldName,mode:"inline",default)
+<$genesis $type={{{ [match[block]then[div]else[span]] }}} class={{{ tc-community-card-field-image [[tc-community-card-field-image-]addsuffix] +[join[ ]] }}}>
+ <%if [has] %>
+ <$transclude $tiddler={{{ [get] }}} $mode=<>/>
+ <%else%>
+ <$transclude $tiddler=<> $mode=<>/>
+ <%endif%>
+$genesis>
+\end community-card-display-transclusion
+
+\procedure community-card-display-text-field(fieldName,showLabel:"yes",linkPrefix,displayPrefix,mode:"block")
+<%if [has] :or[match[title]] %>
+ <$genesis $type={{{ [match[block]then[div]else[span]] }}} class={{{ tc-community-card-field-text [[tc-community-card-field-text-]addsuffix] +[join[ ]] }}}>
+ <%if [match[yes]] %>
+ <$text text=<>/>
+ <%endif%>
+ <%if [!match[]] %>
+ getaddprefix] }}}
+ class="tc-community-card-field-text-value"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ <$text text={{{ [get] :else[match[title]then] +[addprefix] }}}/>
+
+ <%else%>
+
+ <$text text={{{ [get] :else[match[title]then] +[addprefix] }}}/>
+
+ <%endif%>
+ $genesis>
+<%endif%>
+\end community-card-display-text-field
+
+\procedure community-card-person(title)
+ <$let currentTiddler=<>>
+
PRs must meet these minimum requirements before they can be considered for merging:
The material in the PR must be free of licensing restrictions. Which means that either:
The author must hold the copyright in all of the material themselves
The material must be licensed under a license compatible with TiddlyWiki's BSD license
The author must sign the Contributors License Agreement (see below)
Each PR should only make a single feature change
The title of the PR should be 50 characters or less
The title of the PR should be capitalised, and should not end with a period
The title of the PR should be written in the imperative mood. See below
Adequate explanation in the body of the PR for the motivation and implementation of the change. Focus on the why and what, rather than the how
PRs must be self-contained. Although they can link to material elsewhere, everything needed to understand the intention of the PR should be included
Any visual changes introduced by the PR should be noted and illustrated with before/after screenshots
Documentation as appropriate for end-users or developers
Observe the coding style
Read the developers documentation
Please open a consultation issue prior to investing time in making a large PR
Imperative Mood for PR Titles
The "imperative mood" means written as if giving a command or instruction. See this post for more details, but the gist is that the title of the PR should make sense when used to complete the sentence "If applied, this commit will...". So for example, these are good PR titles:
If applied, this commit will update the contributing guidelines
If applied, this commit will change css-escape-polyfill to a $tw.utils method
If applied, this commit will make it easier to subclass the wikitext parser with a custom rule set
These a poorly worded PR titles:
If applied, this commit will edit text widgets should use default text for missing fields
If applied, this commit will signing the CLA
If applied, this commit will don't crash if options.event is missing
PR titles may also include a short prefix to indicate the subsystem to which they apply. For example:
Menu plugin: Include menu text in aerial rotator
Commenting on Pull Requests
One of the principles of open source is that many pairs of eyes on the code can improve quality. So, we welcome comments and critiques of pending PRs. Conventional Comments has some techniques to help make comments as constructive and actionable as possible. Notably, they recommend prefixing a comment with a label to clarify the intention:
praise
Praises highlight something positive. Try to leave at least one of these comments per review. Do not leave false praise (which can actually be damaging). Do look for something to sincerely praise
nitpick
Nitpicks are small, trivial, but necessary changes. Distinguishing nitpick comments significantly helps direct the reader's attention to comments requiring more involvement
suggestion
Suggestions are specific requests to improve the subject under review. It is assumed that we all want to do what's best, so these comments are never dismissed as “mere suggestions”, but are taken seriously
issue
Issues represent user-facing problems. If possible, it's great to follow this kind of comment with a suggestion
question
Questions are appropriate if you have a potential concern but are not quite sure if it's relevant or not. Asking the author for clarification or investigation can lead to a quick resolution
thought
Thoughts represent an idea that popped up from reviewing. These comments are non-blocking by nature, but they are extremely valuable and can lead to more focused initiatives and mentoring opportunities
chore
Chores are simple tasks that must be done before the subject can be “officially” accepted. Usually, these comments reference some common process. Try to leave a link to the process description so that the reader knows how to resolve the chore
Contributor License Agreement
Like other OpenSource projects, TiddlyWiki5 needs a signed contributor license agreement from individual contributors. This is a legal agreement that allows contributors to assert that they own the copyright of their contribution, and that they agree to license it to the UnaMesa Association (the legal entity that owns TiddlyWiki on behalf of the community).
Ensure that the "branch" dropdown at the top left is set to tiddlywiki-com
Click the "edit" button at the top-right corner (clicking this button will fork the project so you can edit the file)
Add your name at the bottom
eg: Jeremy Ruston, @Jermolene, 2011/11/22
Below the edit box for the CLA text you should see a box labelled Propose file change
Enter a brief title to explain the change (eg, "Signing the CLA")
Click the green button labelled Propose file change
On the following screen, click the green button labelled Create pull request
The CLA documents used for this project were created using Harmony Project Templates. "HA-CLA-I-LIST Version 1.0" for "CLA-individual" and "HA-CLA-E-LIST Version 1.0" for "CLA-entity".
+
PRs must meet these minimum requirements before they can be considered for merging:
The material in the PR must be free of licensing restrictions. Which means that either:
The author must hold the copyright in all of the material themselves
The material must be licensed under a license compatible with TiddlyWiki's BSD license
The author must sign the Contributors License Agreement (see below)
Each PR should only make a single feature change
The title of the PR should be 50 characters or less
The title of the PR should be capitalised, and should not end with a period
The title of the PR should be written in the imperative mood. See below
Adequate explanation in the body of the PR for the motivation and implementation of the change. Focus on the why and what, rather than the how
PRs must be self-contained. Although they can link to material elsewhere, everything needed to understand the intention of the PR should be included
Any visual changes introduced by the PR should be noted and illustrated with before/after screenshots
Documentation as appropriate for end-users or developers
Observe the coding style
Read the developers documentation
Please open a consultation issue prior to investing time in making a large PR
Imperative Mood for PR Titles
The "imperative mood" means written as if giving a command or instruction. See this post for more details, but the gist is that the title of the PR should make sense when used to complete the sentence "If applied, this commit will...". So for example, these are good PR titles:
If applied, this commit will update the contributing guidelines
If applied, this commit will change css-escape-polyfill to a $tw.utils method
If applied, this commit will make it easier to subclass the wikitext parser with a custom rule set
These a poorly worded PR titles:
If applied, this commit will edit text widgets should use default text for missing fields
If applied, this commit will signing the CLA
If applied, this commit will don't crash if options.event is missing
PR titles may also include a short prefix to indicate the subsystem to which they apply. For example:
Menu plugin: Include menu text in aerial rotator
Commenting on Pull Requests
One of the principles of open source is that many pairs of eyes on the code can improve quality. So, we welcome comments and critiques of pending PRs. Conventional Comments has some techniques to help make comments as constructive and actionable as possible. Notably, they recommend prefixing a comment with a label to clarify the intention:
praise
Praises highlight something positive. Try to leave at least one of these comments per review. Do not leave false praise (which can actually be damaging). Do look for something to sincerely praise
nitpick
Nitpicks are small, trivial, but necessary changes. Distinguishing nitpick comments significantly helps direct the reader's attention to comments requiring more involvement
suggestion
Suggestions are specific requests to improve the subject under review. It is assumed that we all want to do what's best, so these comments are never dismissed as “mere suggestions”, but are taken seriously
issue
Issues represent user-facing problems. If possible, it's great to follow this kind of comment with a suggestion
question
Questions are appropriate if you have a potential concern but are not quite sure if it's relevant or not. Asking the author for clarification or investigation can lead to a quick resolution
thought
Thoughts represent an idea that popped up from reviewing. These comments are non-blocking by nature, but they are extremely valuable and can lead to more focused initiatives and mentoring opportunities
chore
Chores are simple tasks that must be done before the subject can be “officially” accepted. Usually, these comments reference some common process. Try to leave a link to the process description so that the reader knows how to resolve the chore
Contributor License Agreement
Like other OpenSource projects, TiddlyWiki5 needs a signed contributor license agreement from individual contributors. This is a legal agreement that allows contributors to assert that they own the copyright of their contribution, and that they agree to license it to the UnaMesa Association (the legal entity that owns TiddlyWiki on behalf of the community).
Ensure that the "branch" dropdown at the top left is set to tiddlywiki-com
Click the "edit" button at the top-right corner (clicking this button will fork the project so you can edit the file)
Add your name at the bottom
eg: Jeremy Ruston, @Jermolene, 2011/11/22
Below the edit box for the CLA text you should see a box labelled Propose file change
Enter a brief title to explain the change (eg, "Signing the CLA")
Click the green button labelled Propose file change
On the following screen, click the green button labelled Create pull request
The CLA documents used for this project were created using Harmony Project Templates. "HA-CLA-I-LIST Version 1.0" for "CLA-individual" and "HA-CLA-E-LIST Version 1.0" for "CLA-entity".
This file was automatically generated by TiddlyWiki5
\ No newline at end of file
diff --git a/core/modules/commander.js b/core-server/commander.js
similarity index 92%
rename from core/modules/commander.js
rename to core-server/commander.js
index b55679a2e..f964bfb01 100644
--- a/core/modules/commander.js
+++ b/core-server/commander.js
@@ -6,10 +6,7 @@ module-type: global
The $tw.Commander class is a command interpreter
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
/*
@@ -102,16 +99,18 @@ Commander.prototype.executeNextCommand = function() {
}
}
if(command.info.synchronous) {
- // Synchronous command
+ // Synchronous command (await thenables)
c = new command.Command(params,this);
err = c.execute();
- if(err) {
+ if(err && typeof err.then === "function") {
+ err.then((e) => { e ? this.callback(e) : this.executeNextCommand(); });
+ } else if(err) {
this.callback(err);
} else {
this.executeNextCommand();
}
} else {
- // Asynchronous command
+ // Asynchronous command (await thenables)
c = new command.Command(params,this,function(err) {
if(err) {
self.callback(err);
@@ -120,7 +119,9 @@ Commander.prototype.executeNextCommand = function() {
}
});
err = c.execute();
- if(err) {
+ if(err && typeof err.then === "function") {
+ err.then((e) => { if(e) this.callback(e); });
+ } else if(err) {
this.callback(err);
}
}
@@ -173,5 +174,3 @@ Commander.initCommands = function(moduleType) {
};
exports.Commander = Commander;
-
-})();
diff --git a/core/modules/commands/build.js b/core-server/commands/build.js
similarity index 88%
rename from core/modules/commands/build.js
rename to core-server/commands/build.js
index 8471119d7..cbb7663f1 100644
--- a/core/modules/commands/build.js
+++ b/core-server/commands/build.js
@@ -6,10 +6,7 @@ module-type: command
Command to build a build target
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -24,7 +21,7 @@ var Command = function(params,commander) {
Command.prototype.execute = function() {
// Get the build targets defined in the wiki
- var buildTargets = $tw.boot.wikiInfo.build;
+ var buildTargets = $tw.boot.wikiInfo && $tw.boot.wikiInfo.build;
if(!buildTargets) {
return "No build targets defined";
}
@@ -48,5 +45,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/clearpassword.js b/core-server/commands/clearpassword.js
similarity index 85%
rename from core/modules/commands/clearpassword.js
rename to core-server/commands/clearpassword.js
index 9f714a3ef..915c60d23 100644
--- a/core/modules/commands/clearpassword.js
+++ b/core-server/commands/clearpassword.js
@@ -6,10 +6,7 @@ module-type: command
Clear password for crypto operations
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -29,5 +26,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/commands.js b/core-server/commands/commands.js
similarity index 81%
rename from core/modules/commands/commands.js
rename to core-server/commands/commands.js
index 813f19064..5768ec343 100644
--- a/core/modules/commands/commands.js
+++ b/core-server/commands/commands.js
@@ -7,10 +7,6 @@ Runs the commands returned from a filter
\*/
-(function() {
-
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -29,7 +25,7 @@ Command.prototype.execute = function() {
if(!filter) {
return "No filter specified";
}
- var commands = this.commander.wiki.filterTiddlers(filter)
+ var commands = this.commander.wiki.filterTiddlers(filter);
if(commands.length === 0) {
return "No tiddlers found for filter '" + filter + "'";
}
@@ -38,5 +34,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/deletetiddlers.js b/core-server/commands/deletetiddlers.js
similarity index 83%
rename from core/modules/commands/deletetiddlers.js
rename to core-server/commands/deletetiddlers.js
index 3d8b855d9..c322d7c21 100644
--- a/core/modules/commands/deletetiddlers.js
+++ b/core-server/commands/deletetiddlers.js
@@ -6,10 +6,7 @@ module-type: command
Command to delete tiddlers
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -27,8 +24,7 @@ Command.prototype.execute = function() {
if(this.params.length < 1) {
return "Missing filter";
}
- var self = this,
- wiki = this.commander.wiki,
+ var wiki = this.commander.wiki,
filter = this.params[0],
tiddlers = wiki.filterTiddlers(filter);
$tw.utils.each(tiddlers,function(title) {
@@ -38,5 +34,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/editions.js b/core-server/commands/editions.js
similarity index 90%
rename from core/modules/commands/editions.js
rename to core-server/commands/editions.js
index cc802b9f5..c46489d09 100644
--- a/core/modules/commands/editions.js
+++ b/core-server/commands/editions.js
@@ -6,10 +6,7 @@ module-type: command
Command to list the available editions
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -35,5 +32,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/fetch.js b/core-server/commands/fetch.js
similarity index 77%
rename from core/modules/commands/fetch.js
rename to core-server/commands/fetch.js
index 07cda691c..f7a4e9207 100644
--- a/core/modules/commands/fetch.js
+++ b/core-server/commands/fetch.js
@@ -6,10 +6,7 @@ module-type: command
Commands to fetch external tiddlers
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -69,7 +66,7 @@ Command.prototype.fetchFiles = function(options) {
// Get the list of URLs
var urls;
if(options.url) {
- urls = [options.url]
+ urls = [options.url];
} else if(options.urlFilter) {
urls = this.commander.wiki.filterTiddlers(options.urlFilter);
} else {
@@ -99,30 +96,30 @@ Command.prototype.fetchFile = function(url,options,callback,redirectCount) {
var self = this,
lib = url.substr(0,8) === "https://" ? require("https") : require("http");
lib.get(url).on("response",function(response) {
- var type = (response.headers["content-type"] || "").split(";")[0],
- data = [];
- self.commander.write("Reading " + url + ": ");
- response.on("data",function(chunk) {
- data.push(chunk);
- self.commander.write(".");
- });
- response.on("end",function() {
- self.commander.write("\n");
- if(response.statusCode === 200) {
- self.processBody(Buffer.concat(data),type,options,url);
- callback(null);
- } else {
- if(response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) {
- return self.fetchFile(response.headers.location,options,callback,redirectCount + 1);
- } else {
- return callback("Error " + response.statusCode + " retrieving " + url)
- }
- }
- });
- response.on("error",function(e) {
+ var type = (response.headers["content-type"] || "").split(";")[0],
+ data = [];
+ self.commander.write("Reading " + url + ": ");
+ response.on("data",function(chunk) {
+ data.push(chunk);
+ self.commander.write(".");
+ });
+ response.on("end",function() {
+ self.commander.write("\n");
+ if(response.statusCode === 200) {
+ self.processBody(Buffer.concat(data),type,options,url);
+ callback(null);
+ } else {
+ if(response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) {
+ return self.fetchFile(response.headers.location,options,callback,redirectCount + 1);
+ } else {
+ return callback("Error " + response.statusCode + " retrieving " + url);
+ }
+ }
+ });
+ response.on("error",function(e) {
console.log("Error on GET request: " + e);
callback(e);
- });
+ });
});
return null;
};
@@ -156,20 +153,18 @@ Command.prototype.processBody = function(body,type,options,url) {
if(options.transformFilter) {
var transformedTitle = (incomingWiki.filterTiddlers(options.transformFilter,null,self.commander.wiki.makeTiddlerIterator([title])) || [""])[0];
if(transformedTitle) {
- self.commander.log("Importing " + title + " as " + transformedTitle)
+ self.commander.log("Importing " + title + " as " + transformedTitle);
newTiddler = new $tw.Tiddler(tiddler,{title: transformedTitle});
}
} else {
- self.commander.log("Importing " + title)
+ self.commander.log("Importing " + title);
newTiddler = tiddler;
}
self.commander.wiki.importTiddler(newTiddler);
count++;
}
});
- self.commander.log("Imported " + count + " tiddlers")
+ self.commander.log("Imported " + count + " tiddlers");
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/help.js b/core-server/commands/help.js
similarity index 90%
rename from core/modules/commands/help.js
rename to core-server/commands/help.js
index 90c190829..861c8f6d8 100644
--- a/core/modules/commands/help.js
+++ b/core-server/commands/help.js
@@ -6,10 +6,7 @@ module-type: command
Help command
\*/
-(function(){
-/*jshint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -37,5 +34,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/import.js b/core-server/commands/import.js
similarity index 88%
rename from core/modules/commands/import.js
rename to core-server/commands/import.js
index 9465c3da1..5a6a34854 100644
--- a/core/modules/commands/import.js
+++ b/core-server/commands/import.js
@@ -6,10 +6,7 @@ module-type: command
Command to import tiddlers from a file
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -25,8 +22,7 @@ var Command = function(params,commander,callback) {
Command.prototype.execute = function() {
var self = this,
- fs = require("fs"),
- path = require("path");
+ fs = require("fs");
if(this.params.length < 2) {
return "Missing parameters";
}
@@ -44,5 +40,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/init.js b/core-server/commands/init.js
similarity index 92%
rename from core/modules/commands/init.js
rename to core-server/commands/init.js
index 2d053ae3c..808241e75 100644
--- a/core/modules/commands/init.js
+++ b/core-server/commands/init.js
@@ -6,10 +6,7 @@ module-type: command
Command to initialise an empty wiki folder
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -23,8 +20,7 @@ var Command = function(params,commander) {
};
Command.prototype.execute = function() {
- var fs = require("fs"),
- path = require("path");
+ var fs = require("fs");
// Check that we don't already have a valid wiki folder
if($tw.boot.wikiTiddlersPath || ($tw.utils.isDirectory($tw.boot.wikiPath) && !$tw.utils.isDirectoryEmpty($tw.boot.wikiPath))) {
return "Wiki folder is not empty";
@@ -55,5 +51,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/listen.js b/core-server/commands/listen.js
similarity index 88%
rename from core/modules/commands/listen.js
rename to core-server/commands/listen.js
index 3c5f6a63a..232e0d0e9 100644
--- a/core/modules/commands/listen.js
+++ b/core-server/commands/listen.js
@@ -6,10 +6,7 @@ module-type: command
Listen for HTTP requests and serve tiddlers
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
var Server = require("$:/core/modules/server/server.js").Server;
@@ -18,11 +15,10 @@ exports.info = {
name: "listen",
synchronous: true,
namedParameterMode: true,
- mandatoryParameters: [],
+ mandatoryParameters: []
};
var Command = function(params,commander,callback) {
- var self = this;
this.params = params;
this.commander = commander;
this.callback = callback;
@@ -44,5 +40,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/load.js b/core-server/commands/load.js
similarity index 86%
rename from core/modules/commands/load.js
rename to core-server/commands/load.js
index 8fd9cba10..c114f5b3f 100644
--- a/core/modules/commands/load.js
+++ b/core-server/commands/load.js
@@ -6,10 +6,7 @@ module-type: command
Command to load tiddlers from a file or directory
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -24,9 +21,7 @@ var Command = function(params,commander,callback) {
};
Command.prototype.execute = function() {
- var self = this,
- fs = require("fs"),
- path = require("path");
+ var self = this;
if(this.params.length < 1) {
return "Missing filename";
}
@@ -47,5 +42,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core-server/commands/makelibrary.js b/core-server/commands/makelibrary.js
new file mode 100644
index 000000000..b8ad2e5b6
--- /dev/null
+++ b/core-server/commands/makelibrary.js
@@ -0,0 +1,40 @@
+/*\
+title: $:/core/modules/commands/makelibrary.js
+type: application/javascript
+module-type: command
+
+Command to pack all of the plugins in the library into a plugin tiddler of type "library"
+
+\*/
+
+"use strict";
+
+exports.info = {
+ name: "makelibrary",
+ synchronous: true
+};
+
+var UPGRADE_LIBRARY_TITLE = "$:/UpgradeLibrary";
+
+var Command = function(params,commander,callback) {
+ this.params = params;
+ this.commander = commander;
+ this.callback = callback;
+};
+
+Command.prototype.execute = function() {
+ var wiki = this.commander.wiki,
+ upgradeLibraryTitle = this.params[0] || UPGRADE_LIBRARY_TITLE,
+ tiddlers = $tw.utils.getAllPlugins();
+ // Save the upgrade library tiddler
+ var pluginFields = {
+ title: upgradeLibraryTitle,
+ type: "application/json",
+ "plugin-type": "library",
+ "text": JSON.stringify({tiddlers: tiddlers})
+ };
+ wiki.addTiddler(new $tw.Tiddler(pluginFields));
+ return null;
+};
+
+exports.Command = Command;
diff --git a/core/modules/commands/output.js b/core-server/commands/output.js
similarity index 82%
rename from core/modules/commands/output.js
rename to core-server/commands/output.js
index 0532f58d7..022c03aa8 100644
--- a/core/modules/commands/output.js
+++ b/core-server/commands/output.js
@@ -6,10 +6,7 @@ module-type: command
Command to set the default output location (defaults to current working directory)
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -24,8 +21,7 @@ var Command = function(params,commander,callback) {
};
Command.prototype.execute = function() {
- var fs = require("fs"),
- path = require("path");
+ var path = require("path");
if(this.params.length < 1) {
return "Missing output path";
}
@@ -34,5 +30,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/password.js b/core-server/commands/password.js
similarity index 86%
rename from core/modules/commands/password.js
rename to core-server/commands/password.js
index 85d53fa33..27139a9ed 100644
--- a/core/modules/commands/password.js
+++ b/core-server/commands/password.js
@@ -6,10 +6,7 @@ module-type: command
Save password for crypto operations
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -32,5 +29,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core-server/commands/render.js b/core-server/commands/render.js
new file mode 100644
index 000000000..dc03a2967
--- /dev/null
+++ b/core-server/commands/render.js
@@ -0,0 +1,63 @@
+/*\
+title: $:/core/modules/commands/render.js
+type: application/javascript
+module-type: command
+
+Render individual tiddlers and save the results to the specified files
+
+\*/
+
+"use strict";
+
+exports.info = {
+ name: "render",
+ synchronous: true
+};
+
+var Command = function(params,commander,callback) {
+ this.params = params;
+ this.commander = commander;
+ this.callback = callback;
+};
+
+Command.prototype.execute = function() {
+ if(this.params.length < 1) {
+ return "Missing tiddler filter";
+ }
+ var self = this,
+ fs = require("fs"),
+ path = require("path"),
+ wiki = this.commander.wiki,
+ tiddlerFilter = this.params[0],
+ filenameFilter = this.params[1] || "[is[tiddler]addsuffix[.html]]",
+ type = this.params[2] || "text/html",
+ template = this.params[3],
+ variableList = this.params.slice(4),
+ tiddlers = wiki.filterTiddlers(tiddlerFilter),
+ variables = Object.create(null);
+ while(variableList.length >= 2) {
+ variables[variableList[0]] = variableList[1];
+ variableList = variableList.slice(2);
+ }
+ $tw.utils.each(tiddlers,function(title) {
+ var filenameResults = wiki.filterTiddlers(filenameFilter,$tw.rootWidget,wiki.makeTiddlerIterator([title]));
+ if(filenameResults.length > 0) {
+ var filepath = path.resolve(self.commander.outputPath,filenameResults[0]);
+ if(self.commander.verbose) {
+ console.log("Rendering \"" + title + "\" to \"" + filepath + "\"");
+ }
+ var parser = wiki.parseTiddler(template || title),
+ widgetNode = wiki.makeWidget(parser,{variables: $tw.utils.extend({},variables,{currentTiddler: title,storyTiddler: title})}),
+ container = $tw.fakeDocument.createElement("div");
+ widgetNode.render(container,null);
+ var text = type === "text/html" ? container.innerHTML : container.textContent;
+ $tw.utils.createFileDirectories(filepath);
+ fs.writeFileSync(filepath,text,"utf8");
+ } else {
+ console.log("Not rendering \"" + title + "\" because the filename filter returned an empty result");
+ }
+ });
+ return null;
+};
+
+exports.Command = Command;
diff --git a/core/modules/commands/rendertiddler.js b/core-server/commands/rendertiddler.js
similarity index 93%
rename from core/modules/commands/rendertiddler.js
rename to core-server/commands/rendertiddler.js
index 41812aea1..2a996c8c1 100755
--- a/core/modules/commands/rendertiddler.js
+++ b/core-server/commands/rendertiddler.js
@@ -6,10 +6,7 @@ module-type: command
Command to render a tiddler and save it to a file
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -53,5 +50,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/rendertiddlers.js b/core-server/commands/rendertiddlers.js
similarity index 92%
rename from core/modules/commands/rendertiddlers.js
rename to core-server/commands/rendertiddlers.js
index 78272cc33..8458dd565 100644
--- a/core/modules/commands/rendertiddlers.js
+++ b/core-server/commands/rendertiddlers.js
@@ -6,14 +6,9 @@ module-type: command
Command to render several tiddlers to a folder of files
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-var widget = require("$:/core/modules/widgets/widget.js");
-
exports.info = {
name: "rendertiddlers",
synchronous: true
@@ -65,5 +60,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core-server/commands/save.js b/core-server/commands/save.js
new file mode 100644
index 000000000..d671ce192
--- /dev/null
+++ b/core-server/commands/save.js
@@ -0,0 +1,62 @@
+/*\
+title: $:/core/modules/commands/save.js
+type: application/javascript
+module-type: command
+
+Saves individual tiddlers in their raw text or binary format to the specified files
+
+\*/
+
+"use strict";
+
+exports.info = {
+ name: "save",
+ synchronous: true
+};
+
+var Command = function(params,commander,callback) {
+ this.params = params;
+ this.commander = commander;
+ this.callback = callback;
+};
+
+Command.prototype.execute = function() {
+ if(this.params.length < 1) {
+ return "Missing filename filter";
+ }
+ var self = this,
+ path = require("path"),
+ result = null,
+ wiki = this.commander.wiki,
+ tiddlerFilter = this.params[0],
+ filenameFilter = this.params[1] || "[is[tiddler]]",
+ tiddlers = wiki.filterTiddlers(tiddlerFilter);
+ $tw.utils.each(tiddlers,function(title) {
+ if(!result) {
+ var tiddler = self.commander.wiki.getTiddler(title);
+ if(tiddler) {
+ var fileInfo = $tw.utils.generateTiddlerFileInfo(tiddler,{
+ directory: path.resolve(self.commander.outputPath),
+ pathFilters: [filenameFilter],
+ wiki: wiki,
+ fileInfo: {
+ overwrite: true
+ }
+ });
+ if(self.commander.verbose) {
+ console.log("Saving \"" + title + "\" to \"" + fileInfo.filepath + "\"");
+ }
+ try {
+ $tw.utils.saveTiddlerToFileSync(tiddler,fileInfo);
+ } catch (err) {
+ result = "Error saving tiddler \"" + title + "\", to file: \"" + fileInfo.filepath + "\"";
+ }
+ } else {
+ result = "Tiddler '" + title + "' not found";
+ }
+ }
+ });
+ return result;
+};
+
+exports.Command = Command;
diff --git a/core/modules/commands/savelibrarytiddlers.js b/core-server/commands/savelibrarytiddlers.js
similarity index 97%
rename from core/modules/commands/savelibrarytiddlers.js
rename to core-server/commands/savelibrarytiddlers.js
index af42d7c8a..431960edd 100644
--- a/core/modules/commands/savelibrarytiddlers.js
+++ b/core-server/commands/savelibrarytiddlers.js
@@ -16,10 +16,7 @@ The pathname specifies the pathname to the folder in which the JSON files should
The skinnylisting specifies the title of the tiddler to which a JSON catalogue of the subtiddlers will be saved. The JSON file contains the same data as the bundle tiddler but with the `text` field removed.
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -94,5 +91,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/savetiddler.js b/core-server/commands/savetiddler.js
similarity index 93%
rename from core/modules/commands/savetiddler.js
rename to core-server/commands/savetiddler.js
index efc484ec7..492fe9f12 100644
--- a/core/modules/commands/savetiddler.js
+++ b/core-server/commands/savetiddler.js
@@ -6,10 +6,7 @@ module-type: command
Command to save the content of a tiddler to a file
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -47,5 +44,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/savetiddlers.js b/core-server/commands/savetiddlers.js
similarity index 85%
rename from core/modules/commands/savetiddlers.js
rename to core-server/commands/savetiddlers.js
index d3b82d726..98234f72f 100644
--- a/core/modules/commands/savetiddlers.js
+++ b/core-server/commands/savetiddlers.js
@@ -6,14 +6,9 @@ module-type: command
Command to save several tiddlers to a folder of files
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-var widget = require("$:/core/modules/widgets/widget.js");
-
exports.info = {
name: "savetiddlers",
synchronous: true
@@ -46,11 +41,9 @@ Command.prototype.execute = function() {
type = tiddler.fields.type || "text/vnd.tiddlywiki",
contentTypeInfo = $tw.config.contentTypeInfo[type] || {encoding: "utf8"},
filename = path.resolve(pathname,$tw.utils.encodeURIComponentExtended(title));
- fs.writeFileSync(filename,tiddler.fields.text,contentTypeInfo.encoding);
+ fs.writeFileSync(filename,tiddler.fields.text || "",contentTypeInfo.encoding);
});
return null;
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/savewikifolder.js b/core-server/commands/savewikifolder.js
similarity index 78%
rename from core/modules/commands/savewikifolder.js
rename to core-server/commands/savewikifolder.js
index 48e9be56a..510ad377d 100644
--- a/core/modules/commands/savewikifolder.js
+++ b/core-server/commands/savewikifolder.js
@@ -5,13 +5,17 @@ module-type: command
Command to save the current wiki as a wiki folder
---savewikifolder []
+--savewikifolder [ [=] ]*
+
+The following options are supported:
+
+* ''filter'': a filter expression defining the tiddlers to be included in the output
+* ''explodePlugins'': set to "no" to suppress exploding plugins into their constituent shadow tiddlers (defaults to "yes")
+
+Supports backward compatibility with --savewikifolder [] [ [=] ]*
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -35,14 +39,28 @@ Command.prototype.execute = function() {
if(this.params.length < 1) {
return "Missing wiki folder path";
}
- var wikifoldermaker = new WikiFolderMaker(this.params[0],this.params[1],this.commander);
+ var regFilter = /^[a-zA-Z0-9\.\-_]+=/g, // dynamic parameters
+ namedParames,
+ tiddlerFilter,
+ options = {};
+ if(regFilter.test(this.params[1])) {
+ namedParames = this.commander.extractNamedParameters(this.params.slice(1));
+ tiddlerFilter = namedParames.filter || "[all[tiddlers]]";
+ } else {
+ namedParames = this.commander.extractNamedParameters(this.params.slice(2));
+ tiddlerFilter = this.params[1];
+ }
+ tiddlerFilter = tiddlerFilter || "[all[tiddlers]]";
+ options.explodePlugins = namedParames.explodePlugins || "yes";
+ var wikifoldermaker = new WikiFolderMaker(this.params[0],tiddlerFilter,this.commander,options);
return wikifoldermaker.save();
};
-function WikiFolderMaker(wikiFolderPath,wikiFilter,commander) {
+function WikiFolderMaker(wikiFolderPath,wikiFilter,commander,options) {
this.wikiFolderPath = wikiFolderPath;
- this.wikiFilter = wikiFilter || "[all[tiddlers]]";
+ this.wikiFilter = wikiFilter;
this.commander = commander;
+ this.explodePlugins = options.explodePlugins;
this.wiki = commander.wiki;
this.savedPaths = []; // So that we can detect filename clashes
}
@@ -58,6 +76,7 @@ WikiFolderMaker.prototype.tiddlersToIgnore = [
"$:/boot/boot.js",
"$:/boot/bootprefix.js",
"$:/core",
+ "$:/core-server",
"$:/library/sjcl.js",
"$:/temp/info-plugin"
];
@@ -93,10 +112,13 @@ WikiFolderMaker.prototype.save = function() {
self.log("Adding built-in plugin: " + libraryDetails.name);
newWikiInfo[libraryDetails.type] = newWikiInfo[libraryDetails.type] || [];
$tw.utils.pushTop(newWikiInfo[libraryDetails.type],libraryDetails.name);
- } else {
+ } else if(self.explodePlugins !== "no") {
// A custom plugin
self.log("Processing custom plugin: " + title);
self.saveCustomPlugin(tiddler);
+ } else if(self.explodePlugins === "no") {
+ self.log("Processing custom plugin to tiddlders folder: " + title);
+ self.saveTiddler("tiddlers", tiddler);
}
} else {
// Ordinary tiddler
@@ -152,13 +174,16 @@ WikiFolderMaker.prototype.saveCustomPlugin = function(pluginTiddler) {
this.saveJSONFile(directory + path.sep + "plugin.info",pluginInfo);
self.log("Writing " + directory + path.sep + "plugin.info: " + JSON.stringify(pluginInfo,null,$tw.config.preferences.jsonSpaces));
var pluginTiddlers = $tw.utils.parseJSONSafe(pluginTiddler.fields.text).tiddlers; // A hashmap of tiddlers in the plugin
- $tw.utils.each(pluginTiddlers,function(tiddler) {
+ $tw.utils.each(pluginTiddlers,function(tiddler,title) {
+ if(!tiddler.title) {
+ tiddler.title = title;
+ }
self.saveTiddler(directory,new $tw.Tiddler(tiddler));
});
};
WikiFolderMaker.prototype.saveTiddler = function(directory,tiddler) {
- var title = tiddler.fields.title, fileInfo, pathFilters, extFilters;
+ var fileInfo, pathFilters, extFilters;
if(this.wiki.tiddlerExists("$:/config/FileSystemPaths")) {
pathFilters = this.wiki.getTiddlerText("$:/config/FileSystemPaths","").split("\n");
}
@@ -194,5 +219,3 @@ WikiFolderMaker.prototype.saveFile = function(filename,encoding,data) {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/server.js b/core-server/commands/server.js
similarity index 92%
rename from core/modules/commands/server.js
rename to core-server/commands/server.js
index 507d1281d..f4d7fc8e9 100644
--- a/core/modules/commands/server.js
+++ b/core-server/commands/server.js
@@ -6,10 +6,7 @@ module-type: command
Deprecated legacy command for serving tiddlers
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
var Server = require("$:/core/modules/server/server.js").Server;
@@ -20,7 +17,6 @@ exports.info = {
};
var Command = function(params,commander,callback) {
- var self = this;
this.params = params;
this.commander = commander;
this.callback = callback;
@@ -51,5 +47,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/setfield.js b/core-server/commands/setfield.js
similarity index 87%
rename from core/modules/commands/setfield.js
rename to core-server/commands/setfield.js
index 3f8ec1d14..b39214ffa 100644
--- a/core/modules/commands/setfield.js
+++ b/core-server/commands/setfield.js
@@ -6,14 +6,9 @@ module-type: command
Command to modify selected tiddlers to set a field to the text of a template tiddler that has been wikified with the selected tiddler as the current tiddler.
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-var widget = require("$:/core/modules/widgets/widget.js");
-
exports.info = {
name: "setfield",
synchronous: true
@@ -29,8 +24,7 @@ Command.prototype.execute = function() {
if(this.params.length < 4) {
return "Missing parameters";
}
- var self = this,
- wiki = this.commander.wiki,
+ var wiki = this.commander.wiki,
filter = this.params[0],
fieldname = this.params[1] || "text",
templatetitle = this.params[2],
@@ -54,5 +48,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/unpackplugin.js b/core-server/commands/unpackplugin.js
similarity index 91%
rename from core/modules/commands/unpackplugin.js
rename to core-server/commands/unpackplugin.js
index 6f85c066f..5e2bd33c0 100644
--- a/core/modules/commands/unpackplugin.js
+++ b/core-server/commands/unpackplugin.js
@@ -6,10 +6,7 @@ module-type: command
Command to extract the shadow tiddlers from within a plugin
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -40,5 +37,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/verbose.js b/core-server/commands/verbose.js
similarity index 87%
rename from core/modules/commands/verbose.js
rename to core-server/commands/verbose.js
index 6b0117829..23b5303c7 100644
--- a/core/modules/commands/verbose.js
+++ b/core-server/commands/verbose.js
@@ -6,10 +6,7 @@ module-type: command
Verbose command
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -30,5 +27,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/version.js b/core-server/commands/version.js
similarity index 84%
rename from core/modules/commands/version.js
rename to core-server/commands/version.js
index 24edc97f7..e14c635de 100644
--- a/core/modules/commands/version.js
+++ b/core-server/commands/version.js
@@ -6,10 +6,7 @@ module-type: command
Version command
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
exports.info = {
@@ -28,5 +25,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/utils/filesystem.js b/core-server/filesystem.js
similarity index 93%
rename from core/modules/utils/filesystem.js
rename to core-server/filesystem.js
index 1ba34323e..e24e3fa67 100644
--- a/core/modules/utils/filesystem.js
+++ b/core-server/filesystem.js
@@ -6,10 +6,7 @@ module-type: utils-node
File system utilities
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
var fs = require("fs"),
@@ -29,7 +26,7 @@ exports.getSubdirectories = function(dirPath) {
}
});
return subdirs;
-}
+};
/*
Recursively (and synchronously) copy a directory and all its content
@@ -49,8 +46,7 @@ exports.copyDirectory = function(srcPath,dstPath) {
}
// Function to copy a folder full of files
var copy = function(srcPath,dstPath) {
- var srcStats = fs.lstatSync(srcPath),
- dstExists = fs.existsSync(dstPath);
+ var srcStats = fs.lstatSync(srcPath);
if(srcStats.isFile()) {
$tw.utils.copyFile(srcPath,dstPath);
} else if(srcStats.isDirectory()) {
@@ -86,7 +82,7 @@ exports.copyFile = function(srcPath,dstPath) {
dstFile = fs.openSync(dstPath,"w"),
bytesRead = 1,
pos = 0;
- while (bytesRead > 0) {
+ while(bytesRead > 0) {
bytesRead = fs.readSync(srcFile,fileBuffer,0,FILE_BUFFER_LENGTH,pos);
fs.writeSync(dstFile,fileBuffer,0,bytesRead);
pos += bytesRead;
@@ -151,7 +147,7 @@ exports.deleteDirectory = function(dirPath) {
fs.unlinkSync(currPath);
}
}
- fs.rmdirSync(dirPath);
+ fs.rmdirSync(dirPath);
}
return null;
};
@@ -238,7 +234,7 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
} else {
// Save as a .tid or a text/binary file plus a .meta file
var tiddlerType = tiddler.fields.type || "text/vnd.tiddlywiki";
- if(tiddlerType === "text/vnd.tiddlywiki" || tiddler.hasField("_canonical_uri")) {
+ if(tiddlerType === "text/vnd.tiddlywiki" || tiddlerType === "text/vnd.tiddlywiki-multiple" || tiddler.hasField("_canonical_uri")) {
// Save as a .tid file
fileInfo.type = "application/x-tiddler";
fileInfo.hasMetaFile = false;
@@ -258,7 +254,7 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
// Overriding to the .tid extension needs special handling
fileInfo.type = "application/x-tiddler";
fileInfo.hasMetaFile = false;
- } else if (metaExt === ".json") {
+ } else if(metaExt === ".json") {
// Overriding to the .json extension needs special handling
fileInfo.type = "application/json";
fileInfo.hasMetaFile = false;
@@ -316,11 +312,13 @@ Options include:
pathFilters: optional array of filters to be used to generate the base path
wiki: optional wiki for evaluating the pathFilters
fileInfo: an existing fileInfo object to check against
+ fileInfo.overwrite: if true, turns off filename clash numbers (defaults to false)
*/
exports.generateTiddlerFilepath = function(title,options) {
var directory = options.directory || "",
extension = options.extension || "",
originalpath = (options.fileInfo && options.fileInfo.originalpath) ? options.fileInfo.originalpath : "",
+ overwrite = options.fileInfo && options.fileInfo.overwrite || false,
filepath;
// Check if any of the pathFilters applies
if(options.pathFilters && options.wiki) {
@@ -346,18 +344,18 @@ exports.generateTiddlerFilepath = function(title,options) {
// Replace any Windows control codes
filepath = filepath.replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i,"_$1_");
// Replace any leading spaces with the same number of underscores
- filepath = filepath.replace(/^ +/,function (u) { return u.replace(/ /g, "_")});
+ filepath = filepath.replace(/^ +/,function (u) { return u.replace(/ /g, "_");});
//If the path does not start with "." or ".." && a path seperator, then
if(!/^\.{1,2}[/\\]/g.test(filepath)) {
// Don't let the filename start with any dots because such files are invisible on *nix
- filepath = filepath.replace(/^\.+/g,function (u) { return u.replace(/\./g, "_")});
+ filepath = filepath.replace(/^\.+/g,function (u) { return u.replace(/\./g, "_");});
}
// Replace any Unicode control codes
filepath = filepath.replace(/[\x00-\x1f\x80-\x9f]/g,"_");
// Replace any characters that can't be used in cross-platform filenames
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^/g,"_"));
// Replace any dots or spaces at the end of the extension with the same number of underscores
- extension = extension.replace(/[\. ]+$/, function (u) { return u.replace(/[\. ]/g, "_")});
+ extension = extension.replace(/[\. ]+$/, function (u) { return u.replace(/[\. ]/g, "_");});
// Truncate the extension if it is too long
if(extension.length > 32) {
extension = extension.substr(0,32);
@@ -381,19 +379,20 @@ exports.generateTiddlerFilepath = function(title,options) {
filepath += char.charCodeAt(0).toString();
});
}
- // Add a uniquifier if the file already exists
- var fullPath, oldPath = (options.fileInfo) ? options.fileInfo.filepath : undefined,
- count = 0;
- do {
- fullPath = path.resolve(directory,filepath + (count ? "_" + count : "") + extension);
- if(oldPath && oldPath == fullPath) {
- break;
- }
- count++;
- } while(fs.existsSync(fullPath));
+ // Add a uniquifier if the file already exists (default)
+ var fullPath = path.resolve(directory, filepath + extension);
+ if(!overwrite) {
+ var oldPath = (options.fileInfo) ? options.fileInfo.filepath : undefined,
+ count = 0;
+ do {
+ fullPath = path.resolve(directory,filepath + (count ? "_" + count : "") + extension);
+ if(oldPath && oldPath == fullPath) break;
+ count++;
+ } while(fs.existsSync(fullPath));
+ }
// If the last write failed with an error, or if path does not start with:
// the resolved options.directory, the resolved wikiPath directory, the wikiTiddlersPath directory,
- // or the 'originalpath' directory, then $tw.utils.encodeURIComponentExtended() and resolve to tiddler directory.
+ // or the 'originalpath' directory, then $tw.utils.encodeURIComponentExtended() and resolve to options.directory.
var writePath = $tw.hooks.invokeHook("th-make-tiddler-path",fullPath,fullPath),
encode = (options.fileInfo || {writeError: false}).writeError == true;
if(!encode) {
@@ -401,7 +400,7 @@ exports.generateTiddlerFilepath = function(title,options) {
writePath.indexOf(path.resolve(directory)) == 0 ||
writePath.indexOf(path.resolve($tw.boot.wikiPath)) == 0 ||
writePath.indexOf(path.resolve($tw.boot.wikiTiddlersPath,originalpath)) == 0 );
- }
+ }
if(encode) {
writePath = path.resolve(directory,$tw.utils.encodeURIComponentExtended(fullPath));
}
@@ -521,12 +520,12 @@ Cleanup old files on disk, by comparing the options values:
*/
exports.cleanupTiddlerFiles = function(options,callback) {
var adaptorInfo = options.adaptorInfo || {},
- bootInfo = options.bootInfo || {},
- title = options.title || "undefined";
+ bootInfo = options.bootInfo || {},
+ title = options.title || "undefined";
if(adaptorInfo.filepath && bootInfo.filepath && adaptorInfo.filepath !== bootInfo.filepath) {
$tw.utils.deleteTiddlerFile(adaptorInfo,function(err) {
if(err) {
- if ((err.code == "EPERM" || err.code == "EACCES") && err.syscall == "unlink") {
+ if((err.code == "EPERM" || err.code == "EACCES") && err.syscall == "unlink") {
// Error deleting the previous file on disk, should fail gracefully
$tw.syncer.displayError("Server desynchronized. Error cleaning up previous file for tiddler: \""+title+"\"",err);
return callback(null,bootInfo);
@@ -540,5 +539,3 @@ exports.cleanupTiddlerFiles = function(options,callback) {
return callback(null,bootInfo);
}
};
-
-})();
diff --git a/core-server/plugin.info b/core-server/plugin.info
new file mode 100644
index 000000000..21560a1ad
--- /dev/null
+++ b/core-server/plugin.info
@@ -0,0 +1,11 @@
+{
+ "title": "$:/core-server",
+ "name": "Core Server Components",
+ "description": "TiddlyWiki5 core server components",
+ "author": "JeremyRuston",
+ "core-version": ">=5.0.0",
+ "platform": "server",
+ "plugin-priority": "0",
+ "list": "readme",
+ "stability": "STABILITY_2_STABLE"
+}
diff --git a/core-server/readme.tid b/core-server/readme.tid
new file mode 100644
index 000000000..23efece01
--- /dev/null
+++ b/core-server/readme.tid
@@ -0,0 +1,7 @@
+title: $:/core-server/readme
+
+This plugin contains TiddlyWiki's core components that are only needed on the server, comprising:
+
+* Commands
+* HTTP server code
+* Utility functions for server
diff --git a/core/modules/server/authenticators/basic.js b/core-server/server/authenticators/basic.js
similarity index 95%
rename from core/modules/server/authenticators/basic.js
rename to core-server/server/authenticators/basic.js
index cd528c485..d42f3c46a 100644
--- a/core/modules/server/authenticators/basic.js
+++ b/core-server/server/authenticators/basic.js
@@ -6,16 +6,11 @@ module-type: authenticator
Authenticator for WWW basic authentication
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
if($tw.node) {
- var util = require("util"),
- fs = require("fs"),
- url = require("url"),
+ var fs = require("fs"),
path = require("path");
}
@@ -90,5 +85,3 @@ BasicAuthenticator.prototype.authenticateRequest = function(request,response,sta
};
exports.AuthenticatorClass = BasicAuthenticator;
-
-})();
diff --git a/core/modules/server/authenticators/header.js b/core-server/server/authenticators/header.js
similarity index 91%
rename from core/modules/server/authenticators/header.js
rename to core-server/server/authenticators/header.js
index 78ae6cb0a..bbcf9a1d4 100644
--- a/core/modules/server/authenticators/header.js
+++ b/core-server/server/authenticators/header.js
@@ -6,10 +6,7 @@ module-type: authenticator
Authenticator for trusted header authentication
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
function HeaderAuthenticator(server) {
@@ -37,11 +34,12 @@ HeaderAuthenticator.prototype.authenticateRequest = function(request,response,st
return false;
} else {
// authenticatedUsername will be undefined for anonymous users
- state.authenticatedUsername = username;
+ if(username) {
+ state.authenticatedUsername = $tw.utils.decodeURIComponentSafe(username);
+ }
return true;
}
};
exports.AuthenticatorClass = HeaderAuthenticator;
-})();
diff --git a/core/modules/server/routes/delete-tiddler.js b/core-server/server/routes/delete-tiddler.js
similarity index 80%
rename from core/modules/server/routes/delete-tiddler.js
rename to core-server/server/routes/delete-tiddler.js
index 85b552599..17db39848 100644
--- a/core/modules/server/routes/delete-tiddler.js
+++ b/core-server/server/routes/delete-tiddler.js
@@ -6,16 +6,16 @@ module-type: route
DELETE /recipes/default/tiddlers/:title
\*/
-(function() {
-
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-exports.method = "DELETE";
+exports.methods = ["DELETE"];
exports.path = /^\/bags\/default\/tiddlers\/(.+)$/;
+exports.info = {
+ priority: 100
+};
+
exports.handler = function(request,response,state) {
var title = $tw.utils.decodeURIComponentSafe(state.params[0]);
state.wiki.deleteTiddler(title);
@@ -24,5 +24,3 @@ exports.handler = function(request,response,state) {
});
response.end();
};
-
-}());
diff --git a/core/modules/server/routes/get-favicon.js b/core-server/server/routes/get-favicon.js
similarity index 77%
rename from core/modules/server/routes/get-favicon.js
rename to core-server/server/routes/get-favicon.js
index 39a391127..ce4bc55ed 100644
--- a/core/modules/server/routes/get-favicon.js
+++ b/core-server/server/routes/get-favicon.js
@@ -6,19 +6,17 @@ module-type: route
GET /favicon.ico
\*/
-(function() {
-
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-exports.method = "GET";
+exports.methods = ["GET"];
exports.path = /^\/favicon.ico$/;
+exports.info = {
+ priority: 100
+};
+
exports.handler = function(request,response,state) {
var buffer = state.wiki.getTiddlerText("$:/favicon.ico","");
state.sendResponse(200,{"Content-Type": "image/x-icon"},buffer,"base64");
};
-
-}());
diff --git a/core-server/server/routes/get-file.js b/core-server/server/routes/get-file.js
new file mode 100644
index 000000000..ec928319c
--- /dev/null
+++ b/core-server/server/routes/get-file.js
@@ -0,0 +1,73 @@
+/*\
+title: $:/core/modules/server/routes/get-file.js
+type: application/javascript
+module-type: route
+
+GET /files/:filepath
+
+\*/
+"use strict";
+
+exports.methods = ["GET"];
+
+exports.path = /^\/files\/(.+)$/;
+
+exports.info = {
+ priority: 100
+};
+
+exports.handler = function(request,response,state) {
+ var path = require("path"),
+ fs = require("fs"),
+ suppliedFilename = $tw.utils.decodeURIComponentSafe(state.params[0]),
+ baseFilename = path.resolve(state.boot.wikiPath,"files"),
+ filename = path.resolve(baseFilename,suppliedFilename),
+ extension = path.extname(filename);
+ // Check that the filename is inside the wiki files folder
+ if(path.relative(baseFilename,filename).indexOf("..") === 0) {
+ return state.sendResponse(404,{"Content-Type": "text/plain"},"File '" + suppliedFilename + "' not found");
+ }
+ fs.stat(filename, function(err, stats) {
+ if(err) {
+ return state.sendResponse(404,{"Content-Type": "text/plain"},"File '" + suppliedFilename + "' not found");
+ } else {
+ var type = ($tw.config.fileExtensionInfo[extension] ? $tw.config.fileExtensionInfo[extension].type : "application/octet-stream"),
+ responseHeaders = {
+ "Content-Type": type,
+ "Accept-Ranges": "bytes"
+ };
+ var rangeHeader = request.headers.range,
+ stream;
+ if(rangeHeader) {
+ // Handle range requests
+ var parts = rangeHeader.replace(/bytes=/, "").split("-"),
+ start = parseInt(parts[0], 10),
+ end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1;
+ // Validate start and end
+ if(isNaN(start) || isNaN(end) || start < 0 || end < start || end >= stats.size) {
+ responseHeaders["Content-Range"] = "bytes */" + stats.size;
+ return response.writeHead(416, responseHeaders).end();
+ }
+ var chunksize = (end - start) + 1;
+ responseHeaders["Content-Range"] = "bytes " + start + "-" + end + "/" + stats.size;
+ responseHeaders["Content-Length"] = chunksize;
+ response.writeHead(206, responseHeaders);
+ stream = fs.createReadStream(filename, {start: start, end: end});
+ } else {
+ responseHeaders["Content-Length"] = stats.size;
+ response.writeHead(200, responseHeaders);
+ stream = fs.createReadStream(filename);
+ }
+ // Common stream error handling
+ stream.on("error", function(err) {
+ if(!response.headersSent) {
+ response.writeHead(500, {"Content-Type": "text/plain"});
+ response.end("Read error");
+ } else {
+ response.destroy();
+ }
+ });
+ stream.pipe(response);
+ }
+ });
+};
diff --git a/core/modules/server/routes/get-index.js b/core-server/server/routes/get-index.js
similarity index 70%
rename from core/modules/server/routes/get-index.js
rename to core-server/server/routes/get-index.js
index f2d01f793..bc3af9675 100644
--- a/core/modules/server/routes/get-index.js
+++ b/core-server/server/routes/get-index.js
@@ -6,22 +6,20 @@ module-type: route
GET /
\*/
-(function() {
-
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-exports.method = "GET";
+exports.methods = ["GET"];
exports.path = /^\/$/;
+exports.info = {
+ priority: 100
+};
+
exports.handler = function(request,response,state) {
var text = state.wiki.renderTiddler(state.server.get("root-render-type"),state.server.get("root-tiddler")),
responseHeaders = {
- "Content-Type": state.server.get("root-serve-type")
- };
+ "Content-Type": state.server.get("root-serve-type")
+ };
state.sendResponse(200,responseHeaders,text);
};
-
-}());
diff --git a/core/modules/server/routes/get-login-basic.js b/core-server/server/routes/get-login-basic.js
similarity index 84%
rename from core/modules/server/routes/get-login-basic.js
rename to core-server/server/routes/get-login-basic.js
index d573a0b5d..e9a73c856 100644
--- a/core/modules/server/routes/get-login-basic.js
+++ b/core-server/server/routes/get-login-basic.js
@@ -6,16 +6,16 @@ module-type: route
GET /login-basic -- force a Basic Authentication challenge
\*/
-(function() {
-
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-exports.method = "GET";
+exports.methods = ["GET"];
exports.path = /^\/login-basic$/;
+exports.info = {
+ priority: 100
+};
+
exports.handler = function(request,response,state) {
if(!state.authenticatedUsername) {
// Challenge if there's no username
@@ -25,12 +25,10 @@ exports.handler = function(request,response,state) {
response.end();
} else {
// Redirect to the root wiki if login worked
- var location = ($tw.syncadaptor && $tw.syncadaptor.host)? $tw.syncadaptor.host: "/";
+ var location = ($tw.syncadaptor && $tw.syncadaptor.host)? $tw.syncadaptor.host: `${state.pathPrefix}/`;
response.writeHead(302,{
Location: location
});
response.end();
}
};
-
-}());
diff --git a/core/modules/server/routes/get-status.js b/core-server/server/routes/get-status.js
similarity index 85%
rename from core/modules/server/routes/get-status.js
rename to core-server/server/routes/get-status.js
index ac35ee874..ed2c52806 100644
--- a/core/modules/server/routes/get-status.js
+++ b/core-server/server/routes/get-status.js
@@ -6,16 +6,16 @@ module-type: route
GET /status
\*/
-(function() {
-
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-exports.method = "GET";
+exports.methods = ["GET"];
exports.path = /^\/status$/;
+exports.info = {
+ priority: 100
+};
+
exports.handler = function(request,response,state) {
var text = JSON.stringify({
username: state.authenticatedUsername || state.server.get("anon-username") || "",
@@ -29,5 +29,3 @@ exports.handler = function(request,response,state) {
});
state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8");
};
-
-}());
diff --git a/core/modules/server/routes/get-tiddler-html.js b/core-server/server/routes/get-tiddler-html.js
similarity index 79%
rename from core/modules/server/routes/get-tiddler-html.js
rename to core-server/server/routes/get-tiddler-html.js
index ab1570e30..0f4d89a7c 100644
--- a/core/modules/server/routes/get-tiddler-html.js
+++ b/core-server/server/routes/get-tiddler-html.js
@@ -6,16 +6,16 @@ module-type: route
GET /:title
\*/
-(function() {
-
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-exports.method = "GET";
+exports.methods = ["GET"];
exports.path = /^\/([^\/]+)$/;
+exports.info = {
+ priority: 100
+};
+
exports.handler = function(request,response,state) {
var title = $tw.utils.decodeURIComponentSafe(state.params[0]),
tiddler = state.wiki.getTiddler(title);
@@ -33,12 +33,10 @@ exports.handler = function(request,response,state) {
}
var text = state.wiki.renderTiddler(renderType,renderTemplate,{parseAsInline: true, variables: {currentTiddler: title}});
- // Naughty not to set a content-type, but it's the easiest way to ensure the browser will see HTML pages as HTML, and accept plain text tiddlers as CSS or JS
- state.sendResponse(200,{},text,"utf8");
+ var headers = {"Content-Type": renderType};
+ state.sendResponse(200,headers,text,"utf8");
} else {
response.writeHead(404);
response.end();
}
};
-
-}());
diff --git a/core/modules/server/routes/get-tiddler.js b/core-server/server/routes/get-tiddler.js
similarity index 91%
rename from core/modules/server/routes/get-tiddler.js
rename to core-server/server/routes/get-tiddler.js
index ae6cfeadc..b5c4e11a0 100644
--- a/core/modules/server/routes/get-tiddler.js
+++ b/core-server/server/routes/get-tiddler.js
@@ -6,16 +6,16 @@ module-type: route
GET /recipes/default/tiddlers/:title
\*/
-(function() {
-
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-exports.method = "GET";
+exports.methods = ["GET"];
exports.path = /^\/recipes\/default\/tiddlers\/(.+)$/;
+exports.info = {
+ priority: 100
+};
+
exports.handler = function(request,response,state) {
var title = $tw.utils.decodeURIComponentSafe(state.params[0]),
tiddler = state.wiki.getTiddler(title),
@@ -42,5 +42,3 @@ exports.handler = function(request,response,state) {
response.end();
}
};
-
-}());
diff --git a/core/modules/server/routes/get-tiddlers-json.js b/core-server/server/routes/get-tiddlers-json.js
similarity index 93%
rename from core/modules/server/routes/get-tiddlers-json.js
rename to core-server/server/routes/get-tiddlers-json.js
index 6f3f07f6a..8c78dad9f 100644
--- a/core/modules/server/routes/get-tiddlers-json.js
+++ b/core-server/server/routes/get-tiddlers-json.js
@@ -6,18 +6,18 @@ module-type: route
GET /recipes/default/tiddlers.json?filter=
\*/
-(function() {
-
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
var DEFAULT_FILTER = "[all[tiddlers]!is[system]sort[title]]";
-exports.method = "GET";
+exports.methods = ["GET"];
exports.path = /^\/recipes\/default\/tiddlers.json$/;
+exports.info = {
+ priority: 100
+};
+
exports.handler = function(request,response,state) {
var filter = state.queryParameters.filter || DEFAULT_FILTER;
if(state.wiki.getTiddlerText("$:/config/Server/AllowAllExternalFilters") !== "yes") {
@@ -46,5 +46,3 @@ exports.handler = function(request,response,state) {
var text = JSON.stringify(tiddlers);
state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8");
};
-
-}());
diff --git a/core/modules/server/routes/put-tiddler.js b/core-server/server/routes/put-tiddler.js
similarity index 89%
rename from core/modules/server/routes/put-tiddler.js
rename to core-server/server/routes/put-tiddler.js
index ff1bd2737..def319bc6 100644
--- a/core/modules/server/routes/put-tiddler.js
+++ b/core-server/server/routes/put-tiddler.js
@@ -6,19 +6,19 @@ module-type: route
PUT /recipes/default/tiddlers/:title
\*/
-(function() {
-
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
-exports.method = "PUT";
+exports.methods = ["PUT"];
exports.path = /^\/recipes\/default\/tiddlers\/(.+)$/;
+exports.info = {
+ priority: 100
+};
+
exports.handler = function(request,response,state) {
var title = $tw.utils.decodeURIComponentSafe(state.params[0]),
- fields = $tw.utils.parseJSONSafe(state.data);
+ fields = $tw.utils.parseJSONSafe(state.data);
// Pull up any subfields in the `fields` object
if(fields.fields) {
$tw.utils.each(fields.fields,function(field,name) {
@@ -48,5 +48,3 @@ exports.handler = function(request,response,state) {
});
response.end();
};
-
-}());
diff --git a/core/modules/server/server.js b/core-server/server/server.js
similarity index 89%
rename from core/modules/server/server.js
rename to core-server/server/server.js
index 258ddfa31..8b4e9765d 100644
--- a/core/modules/server/server.js
+++ b/core-server/server/server.js
@@ -6,20 +6,18 @@ module-type: library
Serve tiddlers over http
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
+let fs, url, path, querystring, crypto, zlib;
+
if($tw.node) {
- var util = require("util"),
- fs = require("fs"),
- url = require("url"),
- path = require("path"),
- querystring = require("querystring"),
- crypto = require("crypto"),
- zlib = require("zlib");
+ fs = require("fs"),
+ url = require("url"),
+ path = require("path"),
+ querystring = require("querystring"),
+ crypto = require("crypto"),
+ zlib = require("zlib");
}
/*
@@ -44,7 +42,9 @@ function Server(options) {
}
}
// Setup the default required plugins
- this.requiredPlugins = this.get("required-plugins").split(',');
+ this.requiredPlugins = this.get("required-plugins").split(",");
+ // Initialise CORS
+ this.corsEnable = this.get("cors-enable") === "yes";
// Initialise CSRF
this.csrfDisable = this.get("csrf-disable") === "yes";
// Initialize Gzip compression
@@ -63,9 +63,9 @@ function Server(options) {
this.authorizationPrincipals = {
readers: (this.get("readers") || authorizedUserName).split(",").map($tw.utils.trim),
writers: (this.get("writers") || authorizedUserName).split(",").map($tw.utils.trim)
- }
+ };
if(this.get("admin") || authorizedUserName !== "(anon)") {
- this.authorizationPrincipals["admin"] = (this.get("admin") || authorizedUserName).split(',').map($tw.utils.trim)
+ this.authorizationPrincipals["admin"] = (this.get("admin") || authorizedUserName).split(",").map($tw.utils.trim);
}
// Load and initialise authenticators
$tw.modules.forEachModuleOfType("authenticator", function(title,authenticatorDefinition) {
@@ -77,6 +77,11 @@ function Server(options) {
// console.log("Loading server route " + title);
self.addRoute(routeDefinition);
});
+ this.routes.sort((a, b) => {
+ const priorityA = a.info?.priority ?? 100,
+ priorityB = b.info?.priority ?? 100;
+ return priorityB - priorityA;
+ });
// Initialise the http vs https
this.listenOptions = null;
this.protocol = "http";
@@ -87,7 +92,7 @@ function Server(options) {
this.listenOptions = {
key: fs.readFileSync(path.resolve(this.boot.wikiPath,tlsKeyFilepath),"utf8"),
cert: fs.readFileSync(path.resolve(this.boot.wikiPath,tlsCertFilepath),"utf8"),
- passphrase: tlsPassphrase || ''
+ passphrase: tlsPassphrase || ""
};
this.protocol = "https";
}
@@ -112,7 +117,7 @@ encoding: the encoding of the data to send (passed to the end method of the resp
*/
function sendResponse(request,response,statusCode,headers,data,encoding) {
if(this.enableBrowserCache && (statusCode == 200)) {
- var hash = crypto.createHash('md5');
+ var hash = crypto.createHash("md5");
// Put everything into the hash that could change and invalidate the data that
// the browser already stored. The headers the data and the encoding.
hash.update(data);
@@ -140,6 +145,11 @@ function sendResponse(request,response,statusCode,headers,data,encoding) {
return;
}
}
+ } else {
+ // RFC 7231, 6.1. Overview of Status Codes:
+ // Browser clients may cache 200, 203, 204, 206, 300, 301,
+ // 404, 405, 410, 414, and 501 unless given explicit cache controls
+ headers["Cache-Control"] = headers["Cache-Control"] || "no-store";
}
/*
If the gzip=yes is set, check if the user agent permits compression. If so,
@@ -202,7 +212,6 @@ Server.prototype.addAuthenticator = function(AuthenticatorClass) {
Server.prototype.findMatchingRoute = function(request,state) {
for(var t=0; t Buffer.from(binstr, "binary").toString("base64");
+
+exports.atob = (b64) => Buffer.from(b64, "base64").toString("binary");
+
+function base64ToBytes(base64) {
+ const binString = exports.atob(base64);
+ return Uint8Array.from(binString, (m) => m.codePointAt(0));
+};
+
+function bytesToBase64(bytes) {
+ const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("");
+ return exports.btoa(binString);
+};
+
+exports.base64EncodeUtf8 = (str) => bytesToBase64(new TextEncoder().encode(str));
+
+exports.base64DecodeUtf8 = (str) => new TextDecoder().decode(base64ToBytes(str));
diff --git a/core/modules/utils/edition-info.js b/core-server/utils/edition-info.js
similarity index 68%
rename from core/modules/utils/edition-info.js
rename to core-server/utils/edition-info.js
index f8a5cab06..2c525e3cc 100644
--- a/core/modules/utils/edition-info.js
+++ b/core-server/utils/edition-info.js
@@ -6,10 +6,7 @@ module-type: utils-node
Information about the available editions
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
var fs = require("fs"),
@@ -29,10 +26,14 @@ exports.getEditionInfo = function() {
for(var entryIndex=0; entryIndex= 0x0001 && codeUnit <= 0x001F) || codeUnit == 0x007F ||
+ // If the character is the first character and is in the range [0-9]
+ // (U+0030 to U+0039), […]
+ (index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
+ // If the character is the second character and is in the range [0-9]
+ // (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
+ (
+ index == 1 &&
+ codeUnit >= 0x0030 && codeUnit <= 0x0039 &&
+ firstCodeUnit == 0x002D
+ )
+ ) {
+ // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
+ result += '\\' + codeUnit.toString(16) + ' ';
+ continue;
+ }
+
+ if (
+ // If the character is the first character and is a `-` (U+002D), and
+ // there is no second character, […]
+ index == 0 &&
+ length == 1 &&
+ codeUnit == 0x002D
+ ) {
+ result += '\\' + string.charAt(index);
+ continue;
+ }
+
+ // If the character is not handled by one of the above rules and is
+ // greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
+ // is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
+ // U+005A), or [a-z] (U+0061 to U+007A), […]
+ if (
+ codeUnit >= 0x0080 ||
+ codeUnit == 0x002D ||
+ codeUnit == 0x005F ||
+ codeUnit >= 0x0030 && codeUnit <= 0x0039 ||
+ codeUnit >= 0x0041 && codeUnit <= 0x005A ||
+ codeUnit >= 0x0061 && codeUnit <= 0x007A
+ ) {
+ // the character itself
+ result += string.charAt(index);
+ continue;
+ }
+
+ // Otherwise, the escaped character.
+ // https://drafts.csswg.org/cssom/#escape-a-character
+ result += '\\' + string.charAt(index);
+
+ }
+ return result;
+ };
+ /* eslint-enable */
+})();
diff --git a/core-server/utils/repository.js b/core-server/utils/repository.js
new file mode 100644
index 000000000..d21e0a59c
--- /dev/null
+++ b/core-server/utils/repository.js
@@ -0,0 +1,45 @@
+/*\
+title: $:/core/modules/utils/repository.js
+type: application/javascript
+module-type: utils
+
+Utilities for working with the TiddlyWiki repository file structure
+
+\*/
+"use strict";
+
+/*
+Get an object containing all the plugins as a hashmap by title of the JSON representation of the plugin
+Options:
+
+ignoreEnvironmentVariables: defaults to false
+*/
+exports.getAllPlugins = function(options) {
+ options = options || {};
+ var path = require("path"),
+ tiddlers = {};
+ // Collect up the library plugins
+ var collectPlugins = function(folder) {
+ var pluginFolders = $tw.utils.getSubdirectories(folder) || [];
+ for(var p=0; p
\ No newline at end of file
+\parameters (size:"22pt")
+
\ No newline at end of file
diff --git a/core/images/add-comment.tid b/core/images/add-comment.tid
index 178221806..a118506ed 100644
--- a/core/images/add-comment.tid
+++ b/core/images/add-comment.tid
@@ -1,4 +1,5 @@
title: $:/core/images/add-comment
tags: $:/tags/Image
-
\ No newline at end of file
+\parameters (size:"22pt")
+
\ No newline at end of file
diff --git a/core/images/advanced-search-button.tid b/core/images/advanced-search-button.tid
index 6fda3fe8b..8e5699c4d 100755
--- a/core/images/advanced-search-button.tid
+++ b/core/images/advanced-search-button.tid
@@ -1,4 +1,5 @@
title: $:/core/images/advanced-search-button
tags: $:/tags/Image
-
\ No newline at end of file
+\parameters (size:"22pt")
+
\ No newline at end of file
diff --git a/core/images/auto-height.tid b/core/images/auto-height.tid
index 78f95418b..76deecbad 100755
--- a/core/images/auto-height.tid
+++ b/core/images/auto-height.tid
@@ -1,4 +1,5 @@
title: $:/core/images/auto-height
tags: $:/tags/Image
-
\ No newline at end of file
+\parameters (size:"22pt")
+
\ No newline at end of file
diff --git a/core/images/blank.tid b/core/images/blank.tid
index 731b55a5a..565ef6bec 100755
--- a/core/images/blank.tid
+++ b/core/images/blank.tid
@@ -1,4 +1,5 @@
title: $:/core/images/blank
tags: $:/tags/Image
-
\ No newline at end of file
+\parameters (size:"22pt")
+