diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 30a0dae7c..000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,15 +0,0 @@
-# Known minified files
-/boot/sjcl.js
-/core/modules/utils/base64-utf8/base64-utf8.module.min.js
-/core/modules/utils/diff-match-patch/diff_match_patch.js
-/plugins/tiddlywiki/async/files/async.min.v1.5.0.js
-/plugins/tiddlywiki/codemirror-autocomplete/files/addon/hint/anyword-hint.js
-/plugins/tiddlywiki/codemirror-autocomplete/files/addon/hint/css-hint.js
-/plugins/tiddlywiki/codemirror-autocomplete/files/addon/hint/html-hint.js
-/plugins/tiddlywiki/codemirror-autocomplete/files/addon/hint/javascript-hint.js
-/plugins/tiddlywiki/codemirror-autocomplete/files/addon/hint/show-hint.js
-/plugins/tiddlywiki/codemirror-autocomplete/files/addon/hint/xml-hint.js
-/plugins/tiddlywiki/codemirror-closebrackets/files/addon/edit/closebrackets.js
-/plugins/tiddlywiki/codemirror-closebrackets/files/addon/edit/matchbrackets.js
-/plugins/tiddlywiki/codemirror-closetag/files/addon/edit/closetag.js
-/plugins/tiddlywiki/codemirror-closetag/files/addon/fold/xml-fold.js
diff --git a/.eslintrc.yml b/.eslintrc.yml
deleted file mode 100644
index 105ca829e..000000000
--- a/.eslintrc.yml
+++ /dev/null
@@ -1,267 +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: 'off'
- 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/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 77d3f5f03..000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,43 +0,0 @@
----
-name: Bug report
-about: Create a report to help us improve TiddlyWiki 5
-title: "[BUG]"
-labels: ''
-assignees: ''
-
----
-
-**Describe the bug**
-A clear and concise description of what the bug is.
-
-**To Reproduce**
-Steps to reproduce the behavior:
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-A clear and concise description of what you expected to happen.
-
-**Screenshots**
-If applicable, add screenshots to help explain your problem.
-
-**TiddlyWiki Configuration (please complete the following information):**
- - 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]
-
-**Additional context**
-Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 000000000..286a842bc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,67 @@
+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:"
+ placeholder: |
+ 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
+ placeholder: |
+ - 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
new file mode 100644
index 000000000..dca23b783
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Discuss feature request
+ url: https://github.com/TiddlyWiki/TiddlyWiki5/discussions
+ about: Open new discussion about new feature
+ - name: Talk.Tiddlywiki Forum
+ url: https://talk.tiddlywiki.org
+ about: Join the Forum
\ No newline at end of file
diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
index e3d0735c3..a1e80f7e3 100644
--- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
+++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -20,3 +20,11 @@ A clear and concise description of any alternative solutions or features you've
Add any other context or screenshots about the feature request here.
If you link to discussions elsewhere then please copy and paste the important text, and don't expect readers to scan the entire discussion to find the relevant part.
+
+## Checklist before requesting a review
+
+- [ ] Illustrate any visual changes (however minor) with before/after screenshots
+- [ ] Self-review of code
+- [ ] Documentation updates (for user-visible changes)
+- [ ] Tests (for core code changes)
+- [ ] Complies with coding style guidelines (for JavaScript code)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f377b3921..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"
@@ -72,3 +79,6 @@ jobs:
- run: "./bin/ci-push.sh"
env:
GH_TOKEN: ${{ secrets.GITHUBPUSHTOKEN }}
+ - run: "./bin/build-tw-org.sh"
+ env:
+ GH_TOKEN: ${{ secrets.GITHUBPUSHTOKEN }}
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..8512a30d4
--- /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@v4
+ 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..8421d99c8
--- /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@v4
+ 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-path-validation.yml b/.github/workflows/pr-path-validation.yml
new file mode 100644
index 000000000..674a9115b
--- /dev/null
+++ b/.github/workflows/pr-path-validation.yml
@@ -0,0 +1,18 @@
+name: Validate PR Paths
+
+on:
+ pull_request_target:
+ types: [opened, reopened, synchronize]
+
+jobs:
+ validate-pr:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Validate PR
+ uses: TiddlyWiki/cerebrus@v4
+ with:
+ pr_number: ${{ github.event.pull_request.number }}
+ repo: ${{ github.repository }}
+ base_ref: ${{ github.base_ref }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 5d7cc4870..412759161 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +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 f896abcac..a68dc0752 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.1.24
+ 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,57 @@ 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.hta For Internet Explorer
+# /empty-external-core.html External core empty
+# /tiddlywikicore-.js Core plugin javascript
node $TW5_BUILD_TIDDLYWIKI \
./editions/empty \
- --verbose \
--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 +145,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 +175,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 +183,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 +198,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 +214,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 +230,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 +238,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,9 +246,16 @@ 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 external \
+ || exit 1
+
+# /editions/twitter-archivist/index.html Twitter Archivist edition
+node $TW5_BUILD_TIDDLYWIKI \
+ ./editions/twitter-archivist \
+ --load $TW5_BUILD_OUTPUT/build.tid \
+ --output $TW5_BUILD_OUTPUT/editions/twitter-archivist/ \
--build index \
|| exit 1
@@ -243,10 +269,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
@@ -254,10 +279,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
@@ -269,43 +293,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
@@ -313,10 +313,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
@@ -324,10 +323,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
@@ -335,13 +333,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
@@ -350,20 +357,19 @@ node $TW5_BUILD_TIDDLYWIKI \
# Delete any existing static content
-rm $TW5_BUILD_OUTPUT/languages/de-AT/static/*
-rm $TW5_BUILD_OUTPUT/languages/de-DE/static/*
-rm $TW5_BUILD_OUTPUT/languages/es-ES/static/*
-rm $TW5_BUILD_OUTPUT/languages/fr-FR/static/*
-rm $TW5_BUILD_OUTPUT/languages/ja-JP/static/*
-rm $TW5_BUILD_OUTPUT/languages/ko-KR/static/*
-rm $TW5_BUILD_OUTPUT/languages/zh-Hans/static/*
-rm $TW5_BUILD_OUTPUT/languages/zh-Hant/static/*
+rm -rf $TW5_BUILD_OUTPUT/languages/de-AT/static/*
+rm -rf $TW5_BUILD_OUTPUT/languages/de-DE/static/*
+rm -rf $TW5_BUILD_OUTPUT/languages/es-ES/static/*
+rm -rf $TW5_BUILD_OUTPUT/languages/fr-FR/static/*
+rm -rf $TW5_BUILD_OUTPUT/languages/ja-JP/static/*
+rm -rf $TW5_BUILD_OUTPUT/languages/ko-KR/static/*
+rm -rf $TW5_BUILD_OUTPUT/languages/zh-Hans/static/*
+rm -rf $TW5_BUILD_OUTPUT/languages/zh-Hant/static/*
# /languages/de-AT/index.html Demo wiki with de-AT language
# /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 \
@@ -373,7 +379,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 \
@@ -383,7 +388,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 \
@@ -393,7 +397,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 \
@@ -403,7 +406,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 \
@@ -413,7 +415,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 \
@@ -423,7 +424,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 \
@@ -433,7 +433,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 \
@@ -447,10 +446,9 @@ 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 \
+ --build library\
|| exit 1
# Delete the temporary build tiddler
diff --git a/bin/build-tw-org.sh b/bin/build-tw-org.sh
new file mode 100755
index 000000000..eb76baa85
--- /dev/null
+++ b/bin/build-tw-org.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+# Build tiddlywiki.org assets.
+
+# Default to the version of TiddlyWiki installed in this repo
+
+if [ -z "$TWORG_BUILD_TIDDLYWIKI" ]; then
+ TWORG_BUILD_TIDDLYWIKI=./tiddlywiki.js
+fi
+
+echo "Using TWORG_BUILD_TIDDLYWIKI as [$TWORG_BUILD_TIDDLYWIKI]"
+
+# Set up the build details
+
+if [ -z "$TWORG_BUILD_DETAILS" ]; then
+ TWORG_BUILD_DETAILS="$(git symbolic-ref --short HEAD)-$(git rev-parse HEAD) from $(git remote get-url origin)"
+fi
+
+echo "Using TWORG_BUILD_DETAILS as [$TWORG_BUILD_DETAILS]"
+
+if [ -z "$TWORG_BUILD_COMMIT" ]; then
+ TWORG_BUILD_COMMIT="$(git rev-parse HEAD)"
+fi
+
+echo "Using TWORG_BUILD_COMMIT as [$TWORG_BUILD_COMMIT]"
+
+# Set up the build output directory
+
+if [ -z "$TWORG_BUILD_OUTPUT" ]; then
+ TWORG_BUILD_OUTPUT=$(mktemp -d)
+fi
+
+mkdir -p $TWORG_BUILD_OUTPUT
+
+if [ ! -d "$TWORG_BUILD_OUTPUT" ]; then
+ echo 'A valid TWORG_BUILD_OUTPUT environment variable must be set'
+ exit 1
+fi
+
+echo "Using TWORG_BUILD_OUTPUT as [$TWORG_BUILD_OUTPUT]"
+
+# Pull existing GitHub pages content
+
+git clone --depth=1 --branch=main "https://github.com/TiddlyWiki/tiddlywiki.org-gh-pages.git" $TWORG_BUILD_OUTPUT
+
+# Make the CNAME file that GitHub Pages requires
+
+echo "tiddlywiki.org" > $TWORG_BUILD_OUTPUT/CNAME
+
+# Delete any existing static content
+
+mkdir -p $TWORG_BUILD_OUTPUT/static
+rm $TWORG_BUILD_OUTPUT/static/*
+
+# Put the build details into a .tid file so that it can be included in each build (deleted at the end of this script)
+
+echo -e -n "title: $:/build\ncommit: $TWORG_BUILD_COMMIT\n\n$TWORG_BUILD_DETAILS\n" > $TWORG_BUILD_OUTPUT/build.tid
+
+######################################################
+#
+# tiddlywiki.org distribution
+#
+######################################################
+
+# /index.html 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 $TWORG_BUILD_TIDDLYWIKI \
+ editions/tw.org \
+ --verbose \
+ --version \
+ --load $TWORG_BUILD_OUTPUT/build.tid \
+ --output $TWORG_BUILD_OUTPUT \
+ --build favicon static index \
+ || exit 1
+
+# Delete the temporary build tiddler
+
+rm $TWORG_BUILD_OUTPUT/build.tid || exit 1
+
+# Push output back to GitHub
+
+# Exit script immediately if any command fails
+set -e
+
+pushd $TWORG_BUILD_OUTPUT
+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/TiddlyWiki/tiddlywiki.org-gh-pages.git" &>/dev/null
+git push deploy main &>/dev/null
+popd
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 28f4f715d..4920ab920 100755
--- a/bin/optimise-svgs.js
+++ b/bin/optimise-svgs.js
@@ -5,52 +5,52 @@ Optimise the SVGs in ./core/images using SVGO from https://github.com/svg/svgo
Install SVGO with the following command in the root of the repo:
-npm install svgo
+npm install svgo@2.3.0
*/
"use strict";
var fs = require("fs"),
path = require("path"),
- SVGO = require("svgo"),
- svgo = new SVGO({
+ { optimize } = require("svgo"),
+ config = {
plugins: [
- {cleanupAttrs: true},
- {removeDoctype: true},
- {removeXMLProcInst: true},
- {removeComments: true},
- {removeMetadata: true},
- {removeTitle: true},
- {removeDesc: true},
- {removeUselessDefs: true},
- {removeEditorsNSData: true},
- {removeEmptyAttrs: true},
- {removeHiddenElems: true},
- {removeEmptyText: true},
- {removeEmptyContainers: true},
- {removeViewBox: false},
- {cleanupEnableBackground: true},
- {convertStyleToAttrs: true},
- {convertColors: true},
- {convertPathData: true},
- {convertTransform: true},
- {removeUnknownsAndDefaults: true},
- {removeNonInheritableGroupAttrs: true},
- {removeUselessStrokeAndFill: true},
- {removeUnusedNS: true},
- {cleanupIDs: true},
- {cleanupNumericValues: true},
- {moveElemsAttrsToGroup: true},
- {moveGroupAttrsToElems: true},
- {collapseGroups: true},
- {removeRasterImages: false},
- {mergePaths: true},
- {convertShapeToPath: true},
- {sortAttrs: true},
- {removeDimensions: false},
- {removeAttrs: {attrs: "(stroke|fill)"}}
+ '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',
+ // 'removeRasterImages',
+ 'mergePaths',
+ 'convertShapeToPath',
+ 'sortAttrs',
+ //'removeDimensions',
+ {name: 'removeAttrs', params: { attrs: '(stroke|fill)' } }
]
- });
+ };
var basepath = "./core/images/",
files = fs.readdirSync(basepath).sort();
@@ -66,12 +66,14 @@ files.forEach(function(filename) {
fakeSVG = body.join("\n");
// A hack to make the new-journal-button work
fakeSVG = fakeSVG.replace("<>","<<now "DD">>");
- svgo.optimize(fakeSVG, {path: filepath}).then(function(result) {
+ config.path = filepath;
+ var result = optimize(fakeSVG,config);
+ if(result) {
var newSVG = header.join("\n") + "\n\n" + result.data.replace("<<now "DD">>","<>");
fs.writeFileSync(filepath,newSVG);
- },function(err) {
+ } else {
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/bin/test.sh b/bin/test.sh
index 2de66b1fd..61c7b7143 100755
--- a/bin/test.sh
+++ b/bin/test.sh
@@ -9,6 +9,7 @@ node ./tiddlywiki.js \
--verbose \
--version \
--rendertiddler $:/core/save/all test.html text/plain \
+ --test \
|| exit 1
echo To run the tests in a browser, open "editions/test/output/test.html"
diff --git a/boot/boot.js b/boot/boot.js
index fbac37d77..6ac64c586 100644
--- a/boot/boot.js
+++ b/boot/boot.js
@@ -68,6 +68,26 @@ $tw.utils.isArrayEqual = function(array1,array2) {
});
};
+/*
+Add an entry to a sorted array if it doesn't already exist, while maintaining the sort order
+*/
+$tw.utils.insertSortedArray = function(array,value) {
+ var low = 0, high = array.length - 1, mid, cmp;
+ while(low <= high) {
+ mid = (low + high) >> 1;
+ cmp = value.localeCompare(array[mid]);
+ if(cmp > 0) {
+ low = mid + 1;
+ } else if(cmp < 0) {
+ high = mid - 1;
+ } else {
+ return array;
+ }
+ }
+ array.splice(low,0,value);
+ return array;
+};
+
/*
Push entries onto an array, removing them first if they already exist in the array
array: array to modify (assumed to be free of duplicates)
@@ -122,15 +142,15 @@ $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 and """ to "
*/
@@ -271,7 +339,7 @@ $tw.utils.getLocationHash = function() {
var idx = href.indexOf('#');
if(idx === -1) {
return "#";
- } else if(idx < href.length-1 && href[idx+1] === '#') {
+ } else if(href.substr(idx + 1,1) === "#" || href.substr(idx + 1,3) === "%23") {
// Special case: ignore location hash if it itself starts with a #
return "#";
} else {
@@ -318,8 +386,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 {
@@ -333,7 +401,7 @@ $tw.utils.stringifyList = function(value) {
var result = new Array(value.length);
for(var t=0, l=value.length; t $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") {
@@ -1808,7 +1947,7 @@ A default set of files for TiddlyWiki to ignore during load.
This matches what NPM ignores, and adds "*.meta" to ignore tiddler
metadata files.
*/
-$tw.boot.excludeRegExp = /^\.DS_Store$|^.*\.meta$|^\..*\.swp$|^\._.*$|^\.git$|^\.hg$|^\.lock-wscript$|^\.svn$|^\.wafpickle-.*$|^CVS$|^npm-debug\.log$/;
+$tw.boot.excludeRegExp = /^\.DS_Store$|^.*\.meta$|^\..*\.swp$|^\._.*$|^\.git$|^\.github$|^\.vscode$|^\.hg$|^\.lock-wscript$|^\.svn$|^\.wafpickle-.*$|^CVS$|^npm-debug\.log$/;
/*
Load all the tiddlers recursively from a directory, including honouring `tiddlywiki.files` files for drawing in external files. Returns an array of {filepath:,type:,tiddlers: [{..fields...}],hasMetaFile:}. Note that no file information is returned for externally loaded tiddlers, just the `tiddlers` property.
@@ -1845,22 +1984,41 @@ filepath: pathname of the directory containing the specification file
$tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
var tiddlers = [];
// Read the specification
- var filesInfo = JSON.parse(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)) {
@@ -1868,26 +2026,32 @@ $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;
case "filename-uri-decoded":
- value = decodeURIComponent(path.basename(filename));
+ value = $tw.utils.decodeURIComponentSafe(path.basename(filename));
break;
case "basename":
value = path.basename(filename,path.extname(filename));
break;
case "basename-uri-decoded":
- value = decodeURIComponent(path.basename(filename,path.extname(filename)));
+ value = $tw.utils.decodeURIComponentSafe(path.basename(filename,path.extname(filename)));
break;
case "extname":
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) {
@@ -1906,6 +2070,20 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
tiddlers.push({tiddlers: fileTiddlers});
}
};
+ // Helper to recursively search subdirectories
+ var getAllFiles = function(dirPath, recurse, arrayOfFiles) {
+ recurse = recurse || false;
+ arrayOfFiles = arrayOfFiles || [];
+ var files = fs.readdirSync(dirPath);
+ files.forEach(function(file) {
+ 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) {
@@ -1915,6 +2093,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
@@ -1929,18 +2108,20 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
// Process directory specifier
var dirPath = path.resolve(filepath,dirSpec.path);
if(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
- var files = fs.readdirSync(dirPath),
+ var files = getAllFiles(dirPath, dirSpec.searchSubdirectories),
fileRegExp = new RegExp(dirSpec.filesRegExp || "^.*$"),
metaRegExp = /^.*\.meta$/;
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/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/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/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/Infrastructure Team.tid b/community/project/teams/Infrastructure Team.tid
new file mode 100644
index 000000000..e531aa421
--- /dev/null
+++ b/community/project/teams/Infrastructure Team.tid
@@ -0,0 +1,14 @@
+title: Infrastructure Team
+tags: Community/Team
+modified: 20250909171928024
+created: 20250909171928024
+team: @MotovunJack
+
+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
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/Newsletter Team.tid b/community/project/teams/Newsletter Team.tid
new file mode 100644
index 000000000..2c20fd219
--- /dev/null
+++ b/community/project/teams/Newsletter Team.tid
@@ -0,0 +1,6 @@
+title: Newsletter Team
+tags: Community/Team
+modified: 20250909171928024
+created: 20250909171928024
+
+The Newsletter Team is responsible for producing the TiddlyWiki Newsletter, a monthly email newsletter that highlights news, updates, and community contributions related to TiddlyWiki.
\ No newline at end of file
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/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/tagCommunityTeam.tid b/community/project/teams/tagCommunityTeam.tid
new file mode 100644
index 000000000..9ae81d76f
--- /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]] [[MultiWikiServer Team]] [[Newsletter Team]] [[Infrastructure 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=<>>
+
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".
Remarks
If you do not own the copyright in the entire work of authorship:
In this case, please clearly state so and provide links and any additional information that clarify under which license the rest of the code is distributed.
+
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 302af525b..a6cdc81c9 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);
}
}
@@ -154,7 +155,7 @@ Commander.prototype.extractNamedParameters = function(params,mandatoryParameters
if(errors.length > 0) {
return errors.join(" and\n");
} else {
- return paramsByName;
+ return paramsByName;
}
};
@@ -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-server/commands/commands.js b/core-server/commands/commands.js
new file mode 100644
index 000000000..ddaa387db
--- /dev/null
+++ b/core-server/commands/commands.js
@@ -0,0 +1,36 @@
+/*\
+title: $:/core/modules/commands/commands.js
+type: application/javascript
+module-type: command
+
+Runs the commands returned from a filter
+
+\*/
+
+"use strict";
+
+exports.info = {
+ name: "commands",
+ synchronous: true
+};
+
+var Command = function(params, commander) {
+ this.params = params;
+ this.commander = commander;
+};
+
+Command.prototype.execute = function() {
+ // Parse the filter
+ var filter = this.params[0];
+ if(!filter) {
+ return "No filter specified";
+ }
+ var commands = this.commander.wiki.filterTiddlers(filter)
+ if(commands.length === 0) {
+ return "No tiddlers found for filter '" + filter + "'";
+ }
+ this.commander.addCommandTokens(commands);
+ return null;
+};
+
+exports.Command = Command;
diff --git a/core/modules/commands/deletetiddlers.js b/core-server/commands/deletetiddlers.js
similarity index 89%
rename from core/modules/commands/deletetiddlers.js
rename to core-server/commands/deletetiddlers.js
index 3d8b855d9..797a6428a 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 = {
@@ -38,5 +35,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 97%
rename from core/modules/commands/fetch.js
rename to core-server/commands/fetch.js
index 8d0997010..be076eb3c 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 = {
@@ -115,7 +112,7 @@ Command.prototype.fetchFile = function(url,options,callback,redirectCount) {
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)
+ return callback("Error " + response.statusCode + " retrieving " + url)
}
}
});
@@ -171,5 +168,3 @@ Command.prototype.processBody = function(body,type,options,url) {
};
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 92%
rename from core/modules/commands/import.js
rename to core-server/commands/import.js
index 9465c3da1..7c7b6740d 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 = {
@@ -44,5 +41,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 51b80f267..ed48a5494 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 = {
@@ -48,12 +45,10 @@ Command.prototype.execute = function() {
}
// Tweak the tiddlywiki.info to remove any included wikis
var packagePath = $tw.boot.wikiPath + "/tiddlywiki.info",
- packageJson = JSON.parse(fs.readFileSync(packagePath));
+ packageJson = $tw.utils.parseJSONSafe(fs.readFileSync(packagePath));
delete packageJson.includeWikis;
fs.writeFileSync(packagePath,JSON.stringify(packageJson,null,$tw.config.preferences.jsonSpaces));
return null;
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/listen.js b/core-server/commands/listen.js
similarity index 90%
rename from core/modules/commands/listen.js
rename to core-server/commands/listen.js
index 3c5f6a63a..80cb18293 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,7 +15,7 @@ exports.info = {
name: "listen",
synchronous: true,
namedParameterMode: true,
- mandatoryParameters: [],
+ mandatoryParameters: []
};
var Command = function(params,commander,callback) {
@@ -44,5 +41,3 @@ Command.prototype.execute = function() {
};
exports.Command = Command;
-
-})();
diff --git a/core/modules/commands/load.js b/core-server/commands/load.js
similarity index 92%
rename from core/modules/commands/load.js
rename to core-server/commands/load.js
index 8fd9cba10..e19ecd59a 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 = {
@@ -47,5 +44,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 89%
rename from core/modules/commands/output.js
rename to core-server/commands/output.js
index 0532f58d7..d43eb2596 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 = {
@@ -34,5 +31,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..c85feda92
--- /dev/null
+++ b/core-server/commands/render.js
@@ -0,0 +1,65 @@
+/*\
+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";
+
+ var widget = require("$:/core/modules/widgets/widget.js");
+
+ 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 1860beb7e..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 = {
@@ -40,6 +37,7 @@ Command.prototype.execute = function() {
$tw.utils.createFileDirectories(filename);
if(template) {
variables.currentTiddler = title;
+ variables.storyTiddler = title;
title = template;
}
if(name && value) {
@@ -52,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 87%
rename from core/modules/commands/rendertiddlers.js
rename to core-server/commands/rendertiddlers.js
index dd875e3eb..f392ba704 100644
--- a/core/modules/commands/rendertiddlers.js
+++ b/core-server/commands/rendertiddlers.js
@@ -6,10 +6,7 @@ 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");
@@ -36,7 +33,7 @@ Command.prototype.execute = function() {
filter = this.params[0],
template = this.params[1],
outputPath = this.commander.outputPath,
- pathname = path.resolve(outputPath,this.params[2]),
+ pathname = path.resolve(outputPath,this.params[2]),
type = this.params[3] || "text/html",
extension = this.params[4] || ".html",
deleteDirectory = (this.params[5] || "").toLowerCase() !== "noclean",
@@ -46,7 +43,7 @@ Command.prototype.execute = function() {
}
$tw.utils.each(tiddlers,function(title) {
var parser = wiki.parseTiddler(template),
- widgetNode = wiki.makeWidget(parser,{variables: {currentTiddler: title}}),
+ widgetNode = wiki.makeWidget(parser,{variables: {currentTiddler: title, storyTiddler: title}}),
container = $tw.fakeDocument.createElement("div");
widgetNode.render(container,null);
var text = type === "text/html" ? container.innerHTML : container.textContent,
@@ -57,7 +54,7 @@ Command.prototype.execute = function() {
exportPath = path.resolve(outputPath,macroPath + extension);
}
}
- var finalPath = exportPath || path.resolve(pathname,encodeURIComponent(title) + extension);
+ var finalPath = exportPath || path.resolve(pathname,$tw.utils.encodeURIComponentExtended(title) + extension);
$tw.utils.createFileDirectories(finalPath);
fs.writeFileSync(finalPath,text,"utf8");
});
@@ -65,5 +62,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..1d9155623
--- /dev/null
+++ b/core-server/commands/save.js
@@ -0,0 +1,63 @@
+/*\
+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,
+ fs = require("fs"),
+ 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 94%
rename from core/modules/commands/savelibrarytiddlers.js
rename to core-server/commands/savelibrarytiddlers.js
index a15bd807c..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 = {
@@ -65,11 +62,11 @@ Command.prototype.execute = function() {
$tw.utils.each(filteredPluginList,function(title) {
var tiddler = containerData.tiddlers[title];
// Save each JSON file and collect the skinny data
- var pathname = path.resolve(self.commander.outputPath,basepath + encodeURIComponent(title) + ".json");
+ var pathname = path.resolve(self.commander.outputPath,basepath + $tw.utils.encodeURIComponentExtended(title) + ".json");
$tw.utils.createFileDirectories(pathname);
fs.writeFileSync(pathname,JSON.stringify(tiddler),"utf8");
// Collect the skinny list data
- var pluginTiddlers = JSON.parse(tiddler.text),
+ var pluginTiddlers = $tw.utils.parseJSONSafe(tiddler.text),
readmeContent = (pluginTiddlers.tiddlers[title + "/readme"] || {}).text,
doesRequireReload = !!self.commander.wiki.doesPluginInfoRequireReload(pluginTiddlers),
iconTiddler = pluginTiddlers.tiddlers[title + "/icon"] || {},
@@ -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 84%
rename from core/modules/commands/savetiddlers.js
rename to core-server/commands/savetiddlers.js
index de29efdb7..0e15d5edc 100644
--- a/core/modules/commands/savetiddlers.js
+++ b/core-server/commands/savetiddlers.js
@@ -6,10 +6,7 @@ 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");
@@ -45,12 +42,10 @@ Command.prototype.execute = function() {
var tiddler = self.commander.wiki.getTiddler(title),
type = tiddler.fields.type || "text/vnd.tiddlywiki",
contentTypeInfo = $tw.config.contentTypeInfo[type] || {encoding: "utf8"},
- filename = path.resolve(pathname,encodeURIComponent(title));
- fs.writeFileSync(filename,tiddler.fields.text,contentTypeInfo.encoding);
+ filename = path.resolve(pathname,$tw.utils.encodeURIComponentExtended(title));
+ 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 77%
rename from core/modules/commands/savewikifolder.js
rename to core-server/commands/savewikifolder.js
index f5cfb9cd7..b17246e86 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,11 +112,14 @@ 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
self.saveTiddler("tiddlers",tiddler);
@@ -151,8 +173,11 @@ WikiFolderMaker.prototype.saveCustomPlugin = function(pluginTiddler) {
pluginInfo = pluginTiddler.getFieldStrings({exclude: ["text","type"]});
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 = JSON.parse(pluginTiddler.fields.text).tiddlers; // A hashmap of tiddlers in the plugin
- $tw.utils.each(pluginTiddlers,function(tiddler) {
+ var pluginTiddlers = $tw.utils.parseJSONSafe(pluginTiddler.fields.text).tiddlers; // A hashmap of tiddlers in the plugin
+ $tw.utils.each(pluginTiddlers,function(tiddler,title) {
+ if(!tiddler.title) {
+ tiddler.title = title;
+ }
self.saveTiddler(directory,new $tw.Tiddler(tiddler));
});
};
@@ -167,10 +192,10 @@ WikiFolderMaker.prototype.saveTiddler = function(directory,tiddler) {
}
var fileInfo = $tw.utils.generateTiddlerFileInfo(tiddler,{
directory: path.resolve(this.wikiFolderPath,directory),
- wiki: this.wiki,
pathFilters: pathFilters,
extFilters: extFilters,
- originalpath: this.wiki.extractTiddlerDataItem("$:/config/OriginalTiddlerPaths",title, "")
+ wiki: this.wiki,
+ fileInfo: {}
});
try {
$tw.utils.saveTiddlerToFileSync(tiddler,fileInfo);
@@ -194,5 +219,3 @@ WikiFolderMaker.prototype.saveFile = function(filename,encoding,data) {
};
exports.Command = Command;
-
-})();
diff --git a/core-server/commands/server.js b/core-server/commands/server.js
new file mode 100644
index 000000000..d1ee487a2
--- /dev/null
+++ b/core-server/commands/server.js
@@ -0,0 +1,50 @@
+/*\
+title: $:/core/modules/commands/server.js
+type: application/javascript
+module-type: command
+
+Deprecated legacy command for serving tiddlers
+
+\*/
+
+"use strict";
+
+var Server = require("$:/core/modules/server/server.js").Server;
+
+exports.info = {
+ name: "server",
+ synchronous: true
+};
+
+var Command = function(params,commander,callback) {
+ var self = this;
+ this.params = params;
+ this.commander = commander;
+ this.callback = callback;
+};
+
+Command.prototype.execute = function() {
+ if(!$tw.boot.wikiTiddlersPath) {
+ $tw.utils.warning("Warning: Wiki folder '" + $tw.boot.wikiPath + "' does not exist or is missing a tiddlywiki.info file");
+ }
+ // Set up server
+ this.server = new Server({
+ wiki: this.commander.wiki,
+ variables: {
+ port: this.params[0],
+ host: this.params[6],
+ "root-tiddler": this.params[1],
+ "root-render-type": this.params[2],
+ "root-serve-type": this.params[3],
+ username: this.params[4],
+ password: this.params[5],
+ "path-prefix": this.params[7],
+ "debug-level": this.params[8]
+ }
+ });
+ var nodeServer = this.server.listen();
+ $tw.hooks.invokeHook("th-server-command-post-start",this.server,nodeServer,"tiddlywiki");
+ return null;
+};
+
+exports.Command = Command;
diff --git a/core/modules/commands/setfield.js b/core-server/commands/setfield.js
similarity index 94%
rename from core/modules/commands/setfield.js
rename to core-server/commands/setfield.js
index 3f8ec1d14..7be8cbdca 100644
--- a/core/modules/commands/setfield.js
+++ b/core-server/commands/setfield.js
@@ -6,10 +6,7 @@ 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");
@@ -54,5 +51,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 84%
rename from core/modules/utils/filesystem.js
rename to core-server/filesystem.js
index b7fe2156c..aa214586a 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"),
@@ -213,13 +210,13 @@ Options include:
extFilters: optional array of filters to be used to generate the base path
wiki: optional wiki for evaluating the pathFilters,
fileInfo: an existing fileInfo to check against
- originalpath: a preferred filepath if no pathFilters match
*/
exports.generateTiddlerFileInfo = function(tiddler,options) {
var fileInfo = {}, metaExt;
// Propagate the isEditableFile flag
- if(options.fileInfo) {
- fileInfo.isEditableFile = options.fileInfo.isEditableFile || false;
+ if(options.fileInfo && !!options.fileInfo.isEditableFile) {
+ fileInfo.isEditableFile = true;
+ fileInfo.originalpath = options.fileInfo.originalpath;
}
// Check if the tiddler has any unsafe fields that can't be expressed in a .tid or .meta file: containing control characters, or leading/trailing whitespace
var hasUnsafeFields = false;
@@ -228,6 +225,7 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
hasUnsafeFields = hasUnsafeFields || /[\x00-\x1F]/mg.test(value);
hasUnsafeFields = hasUnsafeFields || ($tw.utils.trim(value) !== value);
}
+ hasUnsafeFields = hasUnsafeFields || /:|#/mg.test(fieldName);
});
// Check for field values
if(hasUnsafeFields) {
@@ -237,7 +235,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") {
+ 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;
@@ -247,7 +245,7 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
fileInfo.hasMetaFile = true;
}
if(options.extFilters) {
- // Check for extension override
+ // Check for extension overrides
metaExt = $tw.utils.generateTiddlerExtension(tiddler.fields.title,{
extFilters: options.extFilters,
wiki: options.wiki
@@ -279,8 +277,7 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
directory: options.directory,
pathFilters: options.pathFilters,
wiki: options.wiki,
- fileInfo: options.fileInfo,
- originalpath: options.originalpath
+ fileInfo: options.fileInfo
});
return fileInfo;
};
@@ -292,8 +289,7 @@ Options include:
wiki: optional wiki for evaluating the extFilters
*/
exports.generateTiddlerExtension = function(title,options) {
- var self = this,
- extension;
+ var extension;
// Check if any of the extFilters applies
if(options.extFilters && options.wiki) {
$tw.utils.each(options.extFilters,function(filter) {
@@ -317,13 +313,14 @@ 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 self = this,
- directory = options.directory || "",
+ var directory = options.directory || "",
extension = options.extension || "",
- originalpath = options.originalpath || "",
- filepath;
+ 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) {
$tw.utils.each(options.pathFilters,function(filter) {
@@ -336,7 +333,7 @@ exports.generateTiddlerFilepath = function(title,options) {
}
});
}
- if(!filepath && originalpath !== "") {
+ if(!filepath && !!originalpath) {
//Use the originalpath without the extension
var ext = path.extname(originalpath);
filepath = originalpath.substring(0,originalpath.length - ext.length);
@@ -345,29 +342,37 @@ exports.generateTiddlerFilepath = function(title,options) {
// Remove any forward or backward slashes so we don't create directories
filepath = filepath.replace(/\/|\\/g,"_");
}
- //If the path does not start with "." or ".." and a path seperator, then
+ // 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, "_")});
+ //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,"_");
+ 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, "_")});
+ // Truncate the extension if it is too long
+ if(extension.length > 32) {
+ extension = extension.substr(0,32);
}
// If the filepath already ends in the extension then remove it
if(filepath.substring(filepath.length - extension.length) === extension) {
filepath = filepath.substring(0,filepath.length - extension.length);
}
- // Remove any characters that can't be used in cross-platform filenames
- filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^/g,"_"));
// Truncate the filename if it is too long
if(filepath.length > 200) {
filepath = filepath.substr(0,200);
}
- // Truncate the extension if it is too long
- if(extension.length > 32) {
- extension = extension.substr(0,32);
- }
- // If the resulting filename is blank (eg because the title is just punctuation characters)
- if(!filepath) {
+ // If the resulting filename is blank (eg because the title is just punctuation)
+ if(!filepath || /^_+$/g.test(filepath)) {
// ...then just use the character codes of the title
- filepath = "";
+ filepath = "";
$tw.utils.each(title.split(""),function(char) {
if(filepath) {
filepath += "-";
@@ -375,28 +380,30 @@ 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,
+ // 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));
+ 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, or the wikiTiddlersPath directory,
- // then encodeURIComponent() and resolve to tiddler directory
- var writePath = $tw.hooks.invokeHook("th-make-tiddler-path",fullPath),
+ // the resolved options.directory, the resolved wikiPath directory, the wikiTiddlersPath 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) {
- encode = !(fullPath.indexOf(path.resolve(directory)) == 0 ||
- fullPath.indexOf(path.resolve($tw.boot.wikiPath)) == 0 ||
- fullPath.indexOf($tw.boot.wikiTiddlersPath) == 0);
+ encode = !(writePath.indexOf($tw.boot.wikiTiddlersPath) == 0 ||
+ 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,encodeURIComponent(fullPath));
+ writePath = path.resolve(directory,$tw.utils.encodeURIComponentExtended(fullPath));
}
// Return the full path to the file
return writePath;
@@ -413,7 +420,7 @@ exports.saveTiddlerToFile = function(tiddler,fileInfo,callback) {
if(fileInfo.hasMetaFile) {
// Save the tiddler as a separate body and meta file
var typeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/plain"] || {encoding: "utf8"};
- fs.writeFile(fileInfo.filepath,tiddler.fields.text,typeInfo.encoding,function(err) {
+ fs.writeFile(fileInfo.filepath,tiddler.fields.text || "",typeInfo.encoding,function(err) {
if(err) {
return callback(err);
}
@@ -455,7 +462,7 @@ exports.saveTiddlerToFileSync = function(tiddler,fileInfo) {
if(fileInfo.hasMetaFile) {
// Save the tiddler as a separate body and meta file
var typeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/plain"] || {encoding: "utf8"};
- fs.writeFileSync(fileInfo.filepath,tiddler.fields.text,typeInfo.encoding);
+ fs.writeFileSync(fileInfo.filepath,tiddler.fields.text || "",typeInfo.encoding);
fs.writeFileSync(fileInfo.filepath + ".meta",tiddler.getFieldStringBlock({exclude: ["text","bag"]}),"utf8");
} else {
// Save the tiddler as a self contained templated file
@@ -465,6 +472,7 @@ exports.saveTiddlerToFileSync = function(tiddler,fileInfo) {
fs.writeFileSync(fileInfo.filepath,JSON.stringify([tiddler.getFieldStrings({exclude: ["bag"]})],null,$tw.config.preferences.jsonSpaces),"utf8");
}
}
+ return fileInfo;
};
/*
@@ -481,7 +489,7 @@ exports.deleteTiddlerFile = function(fileInfo,callback) {
fs.unlink(fileInfo.filepath,function(err) {
if(err) {
return callback(err);
- }
+ }
// Delete the metafile if present
if(fileInfo.hasMetaFile && fs.existsSync(fileInfo.filepath + ".meta")) {
fs.unlink(fileInfo.filepath + ".meta",function(err) {
@@ -532,5 +540,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 97%
rename from core/modules/server/authenticators/basic.js
rename to core-server/server/authenticators/basic.js
index cd528c485..e67e701f5 100644
--- a/core/modules/server/authenticators/basic.js
+++ b/core-server/server/authenticators/basic.js
@@ -6,10 +6,7 @@ module-type: authenticator
Authenticator for WWW basic authentication
\*/
-(function(){
-/*jslint node: true, browser: true */
-/*global $tw: false */
"use strict";
if($tw.node) {
@@ -90,5 +87,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-server/server/routes/delete-tiddler.js b/core-server/server/routes/delete-tiddler.js
new file mode 100644
index 000000000..17db39848
--- /dev/null
+++ b/core-server/server/routes/delete-tiddler.js
@@ -0,0 +1,26 @@
+/*\
+title: $:/core/modules/server/routes/delete-tiddler.js
+type: application/javascript
+module-type: route
+
+DELETE /recipes/default/tiddlers/:title
+
+\*/
+"use strict";
+
+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);
+ response.writeHead(204, "OK", {
+ "Content-Type": "text/plain"
+ });
+ response.end();
+};
diff --git a/core-server/server/routes/get-favicon.js b/core-server/server/routes/get-favicon.js
new file mode 100644
index 000000000..ce4bc55ed
--- /dev/null
+++ b/core-server/server/routes/get-favicon.js
@@ -0,0 +1,22 @@
+/*\
+title: $:/core/modules/server/routes/get-favicon.js
+type: application/javascript
+module-type: route
+
+GET /favicon.ico
+
+\*/
+"use strict";
+
+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-server/server/routes/get-index.js b/core-server/server/routes/get-index.js
new file mode 100644
index 000000000..f856bd511
--- /dev/null
+++ b/core-server/server/routes/get-index.js
@@ -0,0 +1,25 @@
+/*\
+title: $:/core/modules/server/routes/get-index.js
+type: application/javascript
+module-type: route
+
+GET /
+
+\*/
+"use strict";
+
+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")
+ };
+ 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 75%
rename from core/modules/server/routes/get-login-basic.js
rename to core-server/server/routes/get-login-basic.js
index c3cb16eb6..e9a73c856 100644
--- a/core/modules/server/routes/get-login-basic.js
+++ b/core-server/server/routes/get-login-basic.js
@@ -6,30 +6,29 @@ 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
response.writeHead(401,{
"WWW-Authenticate": 'Basic realm="Please provide your username and password to login to ' + state.server.servername + '"'
});
- response.end();
+ response.end();
} else {
// Redirect to the root wiki if login worked
+ var location = ($tw.syncadaptor && $tw.syncadaptor.host)? $tw.syncadaptor.host: `${state.pathPrefix}/`;
response.writeHead(302,{
- Location: "/"
+ Location: location
});
response.end();
}
};
-
-}());
diff --git a/core-server/server/routes/get-status.js b/core-server/server/routes/get-status.js
new file mode 100644
index 000000000..ed2c52806
--- /dev/null
+++ b/core-server/server/routes/get-status.js
@@ -0,0 +1,31 @@
+/*\
+title: $:/core/modules/server/routes/get-status.js
+type: application/javascript
+module-type: route
+
+GET /status
+
+\*/
+"use strict";
+
+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") || "",
+ anonymous: !state.authenticatedUsername,
+ read_only: !state.server.isAuthorized("writers",state.authenticatedUsername),
+ logout_is_available: false,
+ space: {
+ recipe: "default"
+ },
+ tiddlywiki_version: $tw.version
+ });
+ 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 85%
rename from core/modules/server/routes/get-tiddler-html.js
rename to core-server/server/routes/get-tiddler-html.js
index 4fe440821..b7a8aa8f6 100644
--- a/core/modules/server/routes/get-tiddler-html.js
+++ b/core-server/server/routes/get-tiddler-html.js
@@ -6,18 +6,18 @@ 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 = decodeURIComponent(state.params[0]),
+ var title = $tw.utils.decodeURIComponentSafe(state.params[0]),
tiddler = state.wiki.getTiddler(title);
if(tiddler) {
var renderType = tiddler.getFieldString("_render_type"),
@@ -32,13 +32,11 @@ exports.handler = function(request,response,state) {
renderTemplate = renderTemplate || state.server.get("tiddler-render-template");
}
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
- response.writeHead(200);
- response.end(text,"utf8");
+ state.sendResponse(200,{},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 78%
rename from core/modules/server/routes/get-tiddler.js
rename to core-server/server/routes/get-tiddler.js
index e125d7055..b5c4e11a0 100644
--- a/core/modules/server/routes/get-tiddler.js
+++ b/core-server/server/routes/get-tiddler.js
@@ -6,18 +6,18 @@ 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 = decodeURIComponent(state.params[0]),
+ var title = $tw.utils.decodeURIComponentSafe(state.params[0]),
tiddler = state.wiki.getTiddler(title),
tiddlerFields = {},
knownFields = [
@@ -36,12 +36,9 @@ exports.handler = function(request,response,state) {
tiddlerFields.revision = state.wiki.getChangeCount(title);
tiddlerFields.bag = "default";
tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki";
- response.writeHead(200, {"Content-Type": "application/json"});
- response.end(JSON.stringify(tiddlerFields),"utf8");
+ state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8");
} else {
response.writeHead(404);
response.end();
}
};
-
-}());
diff --git a/core/modules/server/routes/get-tiddlers-json.js b/core-server/server/routes/get-tiddlers-json.js
similarity index 87%
rename from core/modules/server/routes/get-tiddlers-json.js
rename to core-server/server/routes/get-tiddlers-json.js
index 203900346..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") {
@@ -33,7 +33,6 @@ exports.handler = function(request,response,state) {
}
var excludeFields = (state.queryParameters.exclude || "text").split(","),
titles = state.wiki.filterTiddlers(filter);
- response.writeHead(200, {"Content-Type": "application/json"});
var tiddlers = [];
$tw.utils.each(titles,function(title) {
var tiddler = state.wiki.getTiddler(title);
@@ -45,7 +44,5 @@ exports.handler = function(request,response,state) {
}
});
var text = JSON.stringify(tiddlers);
- response.end(text,"utf8");
+ state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8");
};
-
-}());
diff --git a/core-server/server/routes/put-tiddler.js b/core-server/server/routes/put-tiddler.js
new file mode 100644
index 000000000..55479b570
--- /dev/null
+++ b/core-server/server/routes/put-tiddler.js
@@ -0,0 +1,50 @@
+/*\
+title: $:/core/modules/server/routes/put-tiddler.js
+type: application/javascript
+module-type: route
+
+PUT /recipes/default/tiddlers/:title
+
+\*/
+"use strict";
+
+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);
+ // Pull up any subfields in the `fields` object
+ if(fields.fields) {
+ $tw.utils.each(fields.fields,function(field,name) {
+ fields[name] = field;
+ });
+ delete fields.fields;
+ }
+ // Remove any revision field
+ if(fields.revision) {
+ delete fields.revision;
+ }
+ // If this is a skinny tiddler, it means the client never got the full
+ // version of the tiddler to edit. So we must preserve whatever text
+ // already exists on the server, or else we'll inadvertently delete it.
+ if(fields._is_skinny !== undefined) {
+ var tiddler = state.wiki.getTiddler(title);
+ if(tiddler) {
+ fields.text = tiddler.fields.text;
+ }
+ delete fields._is_skinny;
+ }
+ state.wiki.addTiddler(new $tw.Tiddler(fields,{title: title}));
+ var changeCount = state.wiki.getChangeCount(title).toString();
+ response.writeHead(204, "OK",{
+ Etag: "\"default/" + encodeURIComponent(title) + "/" + changeCount + ":\"",
+ "Content-Type": "text/plain"
+ });
+ response.end();
+};
diff --git a/core-server/server/server.js b/core-server/server/server.js
new file mode 100644
index 000000000..e1741d08b
--- /dev/null
+++ b/core-server/server/server.js
@@ -0,0 +1,378 @@
+/*\
+title: $:/core/modules/server/server.js
+type: application/javascript
+module-type: library
+
+Serve tiddlers over http
+
+\*/
+
+"use strict";
+
+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");
+}
+
+/*
+A simple HTTP server with regexp-based routes
+options: variables - optional hashmap of variables to set (a misnomer - they are really constant parameters)
+ routes - optional array of routes to use
+ wiki - reference to wiki object
+*/
+function Server(options) {
+ var self = this;
+ this.routes = options.routes || [];
+ this.authenticators = options.authenticators || [];
+ this.wiki = options.wiki;
+ this.boot = options.boot || $tw.boot;
+ // Initialise the variables
+ this.variables = $tw.utils.extend({},this.defaultVariables);
+ if(options.variables) {
+ for(var variable in options.variables) {
+ if(options.variables[variable]) {
+ this.variables[variable] = options.variables[variable];
+ }
+ }
+ }
+ // Setup the default required plugins
+ this.requiredPlugins = this.get("required-plugins").split(',');
+ // Initialise CSRF
+ this.csrfDisable = this.get("csrf-disable") === "yes";
+ // Initialize Gzip compression
+ this.enableGzip = this.get("gzip") === "yes";
+ // Initialize browser-caching
+ this.enableBrowserCache = this.get("use-browser-cache") === "yes";
+ // Initialise authorization
+ var authorizedUserName;
+ if(this.get("username") && this.get("password")) {
+ authorizedUserName = this.get("username");
+ } else if(this.get("credentials")) {
+ authorizedUserName = "(authenticated)";
+ } else {
+ authorizedUserName = "(anon)";
+ }
+ 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)
+ }
+ // Load and initialise authenticators
+ $tw.modules.forEachModuleOfType("authenticator", function(title,authenticatorDefinition) {
+ // console.log("Loading authenticator " + title);
+ self.addAuthenticator(authenticatorDefinition.AuthenticatorClass);
+ });
+ // Load route handlers
+ $tw.modules.forEachModuleOfType("route", function(title,routeDefinition) {
+ // 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";
+ var tlsKeyFilepath = this.get("tls-key"),
+ tlsCertFilepath = this.get("tls-cert"),
+ tlsPassphrase = this.get("tls-passphrase");
+ if(tlsCertFilepath && tlsKeyFilepath) {
+ this.listenOptions = {
+ key: fs.readFileSync(path.resolve(this.boot.wikiPath,tlsKeyFilepath),"utf8"),
+ cert: fs.readFileSync(path.resolve(this.boot.wikiPath,tlsCertFilepath),"utf8"),
+ passphrase: tlsPassphrase || ''
+ };
+ this.protocol = "https";
+ }
+ this.transport = require(this.protocol);
+ // Name the server and init the boot state
+ this.servername = $tw.utils.transliterateToSafeASCII(this.get("server-name") || this.wiki.getTiddlerText("$:/SiteTitle") || "TiddlyWiki5");
+ this.boot.origin = this.get("origin")? this.get("origin"): this.protocol+"://"+this.get("host")+":"+this.get("port");
+ this.boot.pathPrefix = this.get("path-prefix") || "";
+}
+
+/*
+Send a response to the client. This method checks if the response must be sent
+or if the client alrady has the data cached. If that's the case only a 304
+response will be transmitted and the browser will use the cached data.
+Only requests with status code 200 are considdered for caching.
+request: request instance passed to the handler
+response: response instance passed to the handler
+statusCode: stauts code to send to the browser
+headers: response headers (they will be augmented with an `Etag` header)
+data: the data to send (passed to the end method of the response instance)
+encoding: the encoding of the data to send (passed to the end method of the response instance)
+*/
+function sendResponse(request,response,statusCode,headers,data,encoding) {
+ if(this.enableBrowserCache && (statusCode == 200)) {
+ 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);
+ hash.update(JSON.stringify(headers));
+ if(encoding) {
+ hash.update(encoding);
+ }
+ var contentDigest = hash.digest("hex");
+ // RFC 7232 section 2.3 mandates for the etag to be enclosed in quotes
+ headers["Etag"] = '"' + contentDigest + '"';
+ headers["Cache-Control"] = "max-age=0, must-revalidate";
+ // Check if any of the hashes contained within the if-none-match header
+ // matches the current hash.
+ // If one matches, do not send the data but tell the browser to use the
+ // cached data.
+ // We do not implement "*" as it makes no sense here.
+ var ifNoneMatch = request.headers["if-none-match"];
+ if(ifNoneMatch) {
+ var matchParts = ifNoneMatch.split(",").map(function(etag) {
+ return etag.replace(/^[ "]+|[ "]+$/g, "");
+ });
+ if(matchParts.indexOf(contentDigest) != -1) {
+ response.writeHead(304,headers);
+ response.end();
+ 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,
+ compress our response if the raw data is bigger than 2k. Compressing less
+ data is inefficient. Note that we use the synchronous functions from zlib
+ to stay in the imperative style. The current `Server` doesn't depend on
+ this, and we may just as well use the async versions.
+ */
+ if(this.enableGzip && (data.length > 2048)) {
+ var acceptEncoding = request.headers["accept-encoding"] || "";
+ if(/\bdeflate\b/.test(acceptEncoding)) {
+ headers["Content-Encoding"] = "deflate";
+ data = zlib.deflateSync(data);
+ } else if(/\bgzip\b/.test(acceptEncoding)) {
+ headers["Content-Encoding"] = "gzip";
+ data = zlib.gzipSync(data);
+ }
+ }
+
+ response.writeHead(statusCode,headers);
+ response.end(data,encoding);
+}
+
+Server.prototype.defaultVariables = {
+ port: "8080",
+ host: "127.0.0.1",
+ "required-plugins": "$:/plugins/tiddlywiki/filesystem,$:/plugins/tiddlywiki/tiddlyweb",
+ "root-tiddler": "$:/core/save/all",
+ "root-render-type": "text/plain",
+ "root-serve-type": "text/html",
+ "tiddler-render-type": "text/html",
+ "tiddler-render-template": "$:/core/templates/server/static.tiddler.html",
+ "system-tiddler-render-type": "text/plain",
+ "system-tiddler-render-template": "$:/core/templates/wikified-tiddler",
+ "debug-level": "none",
+ "gzip": "no",
+ "use-browser-cache": "no"
+};
+
+Server.prototype.get = function(name) {
+ return this.variables[name];
+};
+
+Server.prototype.addRoute = function(route) {
+ this.routes.push(route);
+};
+
+Server.prototype.addAuthenticator = function(AuthenticatorClass) {
+ // Instantiate and initialise the authenticator
+ var authenticator = new AuthenticatorClass(this),
+ result = authenticator.init();
+ if(typeof result === "string") {
+ $tw.utils.error("Error: " + result);
+ } else if(result) {
+ // Only use the authenticator if it initialised successfully
+ this.authenticators.push(authenticator);
+ }
+};
+
+Server.prototype.findMatchingRoute = function(request,state) {
+ for(var t=0; t 0) {
+ if(!this.authenticators[0].authenticateRequest(request,response,state)) {
+ // Bail if we failed (the authenticator will have sent the response)
+ return;
+ }
+ }
+ // Authorize with the authenticated username
+ if(!this.isAuthorized(state.authorizationType,state.authenticatedUsername)) {
+ response.writeHead(401,"'" + state.authenticatedUsername + "' is not authorized to access '" + this.servername + "'");
+ response.end();
+ return;
+ }
+ // Find the route that matches this path
+ var route = self.findMatchingRoute(request,state);
+ // Optionally output debug info
+ if(self.get("debug-level") !== "none") {
+ console.log("Request path:",JSON.stringify(state.urlInfo));
+ console.log("Request headers:",JSON.stringify(request.headers));
+ console.log("authenticatedUsername:",state.authenticatedUsername);
+ }
+ // Return a 404 if we didn't find a route
+ if(!route) {
+ response.writeHead(404);
+ response.end();
+ return;
+ }
+ // Receive the request body if necessary and hand off to the route handler
+ if(route.bodyFormat === "stream" || request.method === "GET" || request.method === "HEAD") {
+ // Let the route handle the request stream itself
+ route.handler(request,response,state);
+ } else if(route.bodyFormat === "string" || !route.bodyFormat) {
+ // Set the encoding for the incoming request
+ request.setEncoding("utf8");
+ var data = "";
+ request.on("data",function(chunk) {
+ data += chunk.toString();
+ });
+ request.on("end",function() {
+ state.data = data;
+ route.handler(request,response,state);
+ });
+ } else if(route.bodyFormat === "buffer") {
+ var data = [];
+ request.on("data",function(chunk) {
+ data.push(chunk);
+ });
+ request.on("end",function() {
+ state.data = Buffer.concat(data);
+ route.handler(request,response,state);
+ })
+ } else {
+ response.writeHead(400,"Invalid bodyFormat " + route.bodyFormat + " in route " + route.method + " " + route.path.source);
+ response.end();
+ }
+};
+
+/*
+Listen for requests
+port: optional port number (falls back to value of "port" variable)
+host: optional host address (falls back to value of "host" variable)
+prefix: optional prefix (falls back to value of "path-prefix" variable)
+*/
+Server.prototype.listen = function(port,host,prefix) {
+ var self = this;
+ // Handle defaults for port and host
+ port = port || this.get("port");
+ host = host || this.get("host");
+ prefix = prefix || this.get("path-prefix") || "";
+ // Check for the port being a string and look it up as an environment variable
+ if(parseInt(port,10).toString() !== port) {
+ port = process.env[port] || 8080;
+ }
+ // Warn if required plugins are missing
+ var missing = [];
+ for (var index=0; index 0) {
+ var error = "Warning: Plugin(s) required for client-server operation are missing.\n"+
+ "\""+ missing.join("\", \"")+"\"";
+ $tw.utils.warning(error);
+ }
+ // Create the server
+ var server;
+ if(this.listenOptions) {
+ server = this.transport.createServer(this.listenOptions,this.requestHandler.bind(this));
+ } else {
+ server = this.transport.createServer(this.requestHandler.bind(this));
+ }
+ // Display the port number after we've started listening (the port number might have been specified as zero, in which case we will get an assigned port)
+ server.on("listening",function() {
+ var address = server.address(),
+ url = self.protocol + "://" + (address.family === "IPv6" ? "[" + address.address + "]" : address.address) + ":" + address.port + prefix;
+ $tw.utils.log("Serving on " + url,"brown/orange");
+ $tw.utils.log("(press ctrl-C to exit)","red");
+ });
+ // Listen
+ return server.listen(port,host);
+};
+
+exports.Server = Server;
diff --git a/core-server/startup/commands.js b/core-server/startup/commands.js
new file mode 100644
index 000000000..3e4e40f45
--- /dev/null
+++ b/core-server/startup/commands.js
@@ -0,0 +1,32 @@
+/*\
+title: $:/core/modules/startup/commands.js
+type: application/javascript
+module-type: startup
+
+Command processing
+
+\*/
+
+"use strict";
+
+// Export name and synchronous status
+exports.name = "commands";
+exports.platforms = ["node"];
+exports.after = ["story"];
+exports.synchronous = false;
+
+exports.startup = function(callback) {
+ // On the server, start a commander with the command line arguments
+ var commander = new $tw.Commander(
+ $tw.boot.argv,
+ function(err) {
+ if(err) {
+ return $tw.utils.error("Error: " + err);
+ }
+ callback();
+ },
+ $tw.wiki,
+ {output: process.stdout, error: process.stderr}
+ );
+ commander.execute();
+};
diff --git a/core-server/utils/edition-info.js b/core-server/utils/edition-info.js
new file mode 100644
index 000000000..2c525e3cc
--- /dev/null
+++ b/core-server/utils/edition-info.js
@@ -0,0 +1,43 @@
+/*\
+title: $:/core/modules/utils/edition-info.js
+type: application/javascript
+module-type: utils-node
+
+Information about the available editions
+
+\*/
+
+"use strict";
+
+var fs = require("fs"),
+ path = require("path");
+
+var editionInfo;
+
+exports.getEditionInfo = function() {
+ if(!editionInfo) {
+ // Enumerate the edition paths
+ var editionPaths = $tw.getLibraryItemSearchPaths($tw.config.editionsPath,$tw.config.editionsEnvVar);
+ editionInfo = {};
+ for(var editionIndex=0; editionIndex
\ 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")
+