1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2026-07-16 16:52:45 +00:00

Compare commits

..

83 Commits

Author SHA1 Message Date
Jeremy Ruston 93abfab6d9 Shut down dynamic store watching after headless commands
The chokidar watchers and syncer poll timer kept the node event loop
alive forever after commands like --render and --build when a dynamic
store was configured. Add a startup module to the filesystem plugin
that runs after the commander completes: if no server was started
(detected via th-server-command-post-start), it closes the watchers,
stops advertising getUpdatedTiddlers so the syncer schedules no more
polls, and drains any pending sync tasks so saves still flush before
the process exits naturally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 14:13:33 +01:00
Jeremy Ruston b92b17bb67 Merge branch 'master' into bidirectional-filesystem 2026-07-15 12:57:58 +01:00
Joshua Fontany 4bf7e9d192 [Bug] Fix the CSV parser blanking a table (#9919)
* Fix the CSV parser blanking a table

getCellInfo read the loop variable i before its declaration, so hoisting made
it undefined and charAt(undefined) read the first character of the whole text.
Any CSV opening with a separator therefore returned every cell empty.

- Test the cell against the start of the cell rather than the hoisted i
- Keep a separator inside a quoted cell literal
- Cover the parser with tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add a change note for the CSV fix

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:26:59 +02:00
Bram Chen 04cd3081e4 Update chinese language files (#9933)
* Update chinese language files

* Add WikiInformation/Generate/Caption in ControlPanel.multids

* Add ChangeNote
2026-07-14 15:50:37 +01:00
saqimtiaz d27716c1e0 chore: fixed erroneous changenote number 2026-07-14 13:17:58 +02:00
Mario Pietsch 7f7f36d986 Skip system tiddlers in back-indexer updates (#9917)
* Skip system tiddlers in back-indexer updates

Fix #9916: editing a system tiddler that links or transcludes another
tiddler made it a backlinks[]/backtranscludes[] source until the next
index rebuild (shipped since v5.3.4).

* Guard BackSubIndexer.update() with isSystemTiddler() on both sides,
  matching the initial scan; this also skips parsing $:/StoryList on
  every navigation
* Add test-back-indexer.js: regression guards plus adversarial probes
  for shadow reveals and hostile __proto__ titles

* Add ChangeNote
2026-07-14 13:10:21 +02:00
Mario Pietsch 86a4e18134 diff-text change newline to ↲ (#9736)
* diff-text change newline to ↲

* Add ChangeNote
2026-07-14 13:04:50 +02:00
Mario Pietsch 304858c7c5 Make Click to generate wiki info button translatable (#9737)
* Make Click to generate wiki info button translatable

* Add ChangeNote
2026-07-14 12:53:23 +02:00
Mario Pietsch 7ececf9e0f Align fakedom tagname with HTML spec (#9843)
* Fakedom: uppercase tagName for HTML elements per DOM spec

Browsers uppercase tagName for HTML elements. Other namespaces (SVG,
MathML, XML) preserve case. Fakedom now matches, so widget code can use
the natural `node.tagName === "OPTGROUP"` form in both environments.

https://dom.spec.whatwg.org/#dom-element-tagname

* Tests: cover fakedom tagName spec compliance

Three specs pin the DOM contract for tagName: uppercase for HTML
elements, case preservation for other namespaces, and non-mutating
reads. Includes the OPTGROUP predicate the select widget will rely on
once it can drop its defensive .toUpperCase() call.

https://dom.spec.whatwg.org/#dom-element-tagname

* Fakedom: empty or null namespace creates a no-namespace element

Per DOM spec, createElementNS("") and createElementNS(null) have to produce
elements with no namespace. Fakedom previously defaults both to HTML,
which made tagName incorrectly uppercase non-HTML elements.
createElement without arguments still defaults to HTML.

https://dom.spec.whatwg.org/#dom-document-createelementns

* Add ChangeNote
2026-07-14 12:22:45 +02:00
saqimtiaz 521e530e11 docs: added size comparison of empty.html from prerelease and latest release 2026-07-14 07:55:48 +02:00
XLBilly 4c29dae4af Replace some wikify widget with call dynamic attribute syntax (#9816)
* Replace some wikify widget with call dynamic attribute syntax

* Apply suggestions

* Add change note
2026-07-13 15:57:04 +02:00
Mario Pietsch f5317dc225 Fix Node.js deprecated warning (#9742)
* Fix Node.js deprecated warning

* Add ChangeNote

* Change deprecation to bugfix for DEP0169 warning

Updated change type from 'deprecation' to 'bugfix' for the Node.js server's handling of the DEP0169 warning.
2026-07-13 13:43:07 +02:00
lin onetwo c952450c2e fix(filter): optimize tag[] and !tag[] with Set for O(1) lookup (#9715)
* fix(filter): optimize tag[] and !tag[] with Set for O(1) lookup, resolves performance gap vs search:tags[]

* useless test

* note

* note
2026-07-13 13:37:05 +02:00
Rob Hoelz 1eb7ec4402 Convert deeply nested if/else to do/while(false) with breaks for parseAttribute (#9770)
01d3cde96 refactored parseMacroParameterAsAttribute to use a
do/while(false) construct with break statements - this updates
parseAttribute with an analogous refactoring for consistency
2026-07-13 08:15:26 +02:00
dependabot[bot] 869557f7d1 Bump js-yaml from 4.1.1 to 4.3.0 (#9903)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-12 09:22:08 +02:00
saqimtiaz c9f1154643 chore: preparing for v5.5.0 2026-07-10 12:41:53 +02:00
Jeremy Ruston 748f04c9e3 Update TiddlyWiki release checklist 2026-07-10 11:01:22 +01:00
saqimtiaz d391595836 Version number update for 5.4.1 2026-07-10 11:39:38 +02:00
saqimtiaz 967140a148 Merge branch 'master' of github.com:TiddlyWiki/TiddlyWiki5 2026-07-10 11:37:07 +02:00
saqimtiaz 95e68b0437 chore: added missing changenotes 2026-07-10 11:36:59 +02:00
Jeremy Ruston 8ddede1611 Missing v5.4.1 release notes 2026-07-10 10:36:20 +01:00
saqimtiaz ceb200b08c chore: update license and readme 2026-07-10 11:17:53 +02:00
saqimtiaz 00e5f48a59 chore: preparing for v5.4.1 release 2026-07-10 11:12:01 +02:00
saqimtiaz ea0e9105bc Merge branch 'tiddlywiki-com' 2026-07-10 11:00:56 +02:00
Jeremy Ruston 51459815ba Fix CI errors 2026-07-09 16:16:36 +01:00
Jeremy Ruston 8c62935a01 Fix incorrect reference to BurningTreeC's website 2026-07-09 14:14:47 +01:00
Jeremy Ruston 839fa2417d Fix CI issues
1. Remove references to obsolete plugins
2. Fix duplicate language string entry
2026-07-09 14:12:10 +01:00
Saq Imtiaz 1b8610e4d8 Fixes issue with copying cross window event properties (#9905)
* fix: issue with copying cross window event properties

* docs: added changenote
2026-07-09 12:50:12 +01:00
Mario Pietsch ca063cbe90 Add quick test runner (#9743) 2026-07-08 13:56:16 +01:00
Jeremy Ruston 7d4328b2fb Merge branch 'master' of https://github.com/TiddlyWiki/TiddlyWiki5 2026-07-08 12:02:55 +01:00
Jeremy Ruston 1ae224ab74 Merge branch 'tiddlywiki-com' 2026-07-08 12:02:50 +01:00
Mario Pietsch c308fc44c4 Fix invalid source positions in parse tree nodes (#9882)
* Fix invalid source positions in parse tree nodes

Several wikitext rules recorded start and end offsets that did not match
the source they represent. The offsets are only used for source position
mapping, not rendering or serialization, so output is unchanged and
source.slice(start, end) now equals the node text.

* import: the filter attribute start was set to the whole source string
  instead of the numeric offset. It used this.parser.source where it
  meant this.parser.pos, so every \import node carried a copy of the
  entire source as its start.

* codeinline: the text node end pointed past the closing backtick, so the
  span included the closing delimiter and could read one or two characters
  too far. It now ends where the content ends.

* wikilinkprefix and extlink: a suppressed link such as ~SomeLink or
  ~http://example.com produced a text node whose span included the
  leading ~ while its text did not. The span now starts after the ~.

* Add 5.4.1 change note for #9882

Document the parse tree source position fixes: inline code, suppressed
external links, suppressed wikilinks, and the import filter rule.

* Add tests for parse tree source positions

Cover the four wikitext parser rules whose `start` and `end` offsets are
corrected in this PR: codeinline, extlink, wikilinkprefix, and import.
2026-07-08 10:03:11 +01:00
Jeremy Ruston aa3fb85919 BurningTreeC is leader of the TiddlyDesktop team
@BurningTreeC please check the details of your card,
2026-07-08 09:58:40 +01:00
Jeremy Ruston bd052a33f8 Correct v5.4.1 banner 2026-07-08 09:45:29 +01:00
superuser-does bb766c36c3 [DOCS] Fix survey image link (#9897) 2026-07-01 15:55:49 +02:00
dependabot[bot] 566a341328 Bump js-yaml from 4.1.0 to 4.1.1 (#9777)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 08:11:15 +01:00
dependabot[bot] 9316234001 Bump picomatch from 4.0.3 to 4.0.4 (#9776)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 4.0.3 to 4.0.4.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 08:11:02 +01:00
dependabot[bot] c1cb93c9df Bump minimatch from 3.1.2 to 3.1.5 (#9775)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 08:10:52 +01:00
dependabot[bot] e46361fbc0 Bump flatted from 3.3.3 to 3.4.2 (#9774)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 08:10:40 +01:00
s793016 e730836d6f Freelinks Plugin: Add configurable MaxLinks cap and remove search truncation (#9836)
* Update aho-corasick.js

Optimized Aho-Corasick string matching algorithm implementation with enhanced
performance and error handling for TiddlyWiki freelinking functionality.
 
- Uses WeakMap for failure links. WeakMap keys are compared by object identity
  (reference equality), which is required here because trie nodes are plain
  objects — a regular {} map would not work because JavaScript only supports
  string and Symbol keys, forcing object keys to be coerced to strings.
- Outputs are merged at build time (classic AC optimization), eliminating the
  need to walk the failure chain during search.
- search() converts case per character to avoid Unicode index desync (e.g.
  Turkish İ expands under toLowerCase(), shifting subsequent indices).
- No match count cap in search(); truncation is handled at the render stage
  by processTextWithMatches() to avoid silently dropping matches mid-text.
- Optional word boundary filtering: CJK always allowed; Latin requires
  non-word characters on both sides.

* Update text.js

- Render output capped by $:/config/Freelinks/MaxLinks (default 500).
  The cap applies to the final set of non-overlapping rendered links, not to
  the raw search results: search always runs to completion so that the
  longest-match selection has full information. Text beyond the cap remains
  as plain text rather than being silently omitted.

* Create config-Freelinks-MAX_LINKS_TIDDLER.tid

setting for MAX_LINKS_TIDDLER

* Update and rename config-Freelinks-MAX_LINKS_TIDDLER.tid to config-Freelinks-MaxLinks.tid

fix typo

* Update settings.tid

add MaxLinks setting

* Update readme.tid

add MaxLinks setting text

* Create #9836.tid

release note

* Update tiddlywiki.info

add freelinks for test build

* Update macros-view.tid

fix structure problem

* Update text.js

add IGNORE_CASE_TIDDLER  to whitelist

* Rename #9836.tid to #9836.tid

move to 5.4.1

* Update text.js

I deleted the wrong line last time, so I'm putting it back.

* Update text.js

eslint fix

* Update text.js

refactor: inline plain-text via dynamic core reference

* Delete plugins/tiddlywiki/freelinks/plain-text.js

remove plain-text.js: now inlined via dynamic core reference

* Update text.js

restore plain-text.js, remove new Function extraction

* Add files via upload

restore plain-text.js, remove new Function extraction

* Update #9836.tid

update new bug

* Update #9836.tid

tw version fix

* Update tiddlywiki.info

reverse to original
2026-06-20 19:25:19 +01:00
XLBilly d050dd30cc Fix KaTeX missing font (#9873) 2026-06-09 16:36:22 +01:00
IchijikuIchigo 635f6b5e76 [ja_JP] Update of Japanese translations (#9870)
* [ja_JP] Japanese translation update from commit: 3ed481b

* [ja_JP] Japanese translation update from commit: ad3587b

* [ja_JP] Japanese translation update from commit: 6bc77cf

* [ja_JP] Japanese translation update from commit: 5d1c1ea, c4991a5

* [ja_JP] Japanese translation update from commit: 37a4613

* [ja_JP] Japanese translation update from commit: 748ef8a

* [ja_JP] Japanese translation update from commit: 92caa73

* [ja_JP] Japanese translation update from commit: 87ba87b

* [ja_JP] Japanese translation update from commit: 15ba415

* [ja_JP] Japanese translation update from commit: 64ee20e

* [ja_JP] Japanese translation update from commit: 5d1c1ea,add3d42

* [ja_JP] Japanese translation update from commit: 64ee20e

* [ja_JP] Japanese translation update from commit: b001333

* [ja_JP] Japanese translation update from commit: b434b9d

* [ja_JP] Japanese translation update from commit: 3983086
2026-06-05 16:23:47 +01:00
Saq Imtiaz 40d0d542f7 playwright: revert timeout to 60s 2026-06-04 09:46:36 +02:00
Saq Imtiaz a66d9a596c playwright: change timeout to 45s 2026-06-04 09:37:21 +02:00
Saq Imtiaz 23063ea9c8 playwright: increase timeout to 60s 2026-06-04 09:32:23 +02:00
Jeremy Ruston 9c1f69e9c1 Revert "[ja_JP] Update of Japanese translations (#9853)"
Reverting as was inadvertently merged to tiddlywiki-com

This reverts commit 14b11575d0.
2026-06-04 08:28:09 +01:00
Saq Imtiaz 6abfcb631d Update playwright.config.js to address build failures 2026-06-04 09:26:56 +02:00
IchijikuIchigo 14b11575d0 [ja_JP] Update of Japanese translations (#9853)
* [ja_JP] Japanese translation update from commit: 3ed481b

* [ja_JP] Japanese translation update from commit: ad3587b

* [ja_JP] Japanese translation update from commit: 6bc77cf

* [ja_JP] Japanese translation update from commit: 5d1c1ea, c4991a5

* [ja_JP] Japanese translation update from commit: 37a4613

* [ja_JP] Japanese translation update from commit: 748ef8a

* [ja_JP] Japanese translation update from commit: 92caa73

* [ja_JP] Japanese translation update from commit: 87ba87b

* [ja_JP] Japanese translation update from commit: 15ba415

* [ja_JP] Japanese translation update from commit: 64ee20e

* [ja_JP] Japanese translation update from commit: 5d1c1ea,add3d42

* [ja_JP] Japanese translation update from commit: 64ee20e

* [ja_JP] Japanese translation update from commit: b001333

* [ja_JP] Japanese translation update from commit: b434b9d

* [ja_JP] Japanese translation update from commit: 3983086
2026-06-04 08:25:12 +01:00
Maurycy Zarzycki c9b1fbbb76 Add/update translations to Polish language: (#9829)
- 0e765bd
- 9c09841
- 33b2f51
- cd8b1fa
- 0177f09
- b673651
- 9afe522
2026-06-04 08:24:43 +01:00
Jeremy Ruston 27c568e53e Try increasing Firefox timeout because of build errors 2026-06-04 08:21:35 +01:00
Jeremy Ruston 0023d3f6fa Update license date 2026-06-04 08:03:01 +01:00
Jeremy Ruston 76ebe9d8e9 Merge branch 'tiddlywiki-com' 2026-05-17 16:09:36 +01:00
Sean Clayton 40444eade5 Signing CLA (#9856) 2026-05-17 15:15:54 +01:00
Jeremy Ruston fe8723be49 Prepare for v5.4.1, not v5.5.0
See discussion at #9832
2026-05-08 18:13:41 +01:00
Mario Pietsch 56ea0789e1 Update deprecated.js issues (#9833)
* Fix RSOD when $tw.utils.addClass receives a class string with whitespace

PR #9251 replaced the manual setAttribute("class", ...) implementation of
$tw.utils.addClass/removeClass/toggleClass with direct Element.classList
calls. Unlike setAttribute, classList.add/remove/toggle throws
InvalidCharacterError on any token containing whitespace, so callers that
pass a whole class string (e.g. modal.js passing tiddler.fields.class)
now crash.

Manual repro on tw5-com: open SampleWizard, set the `class` field to
"aaa bbb", Done, open popup -> OK -> open nested popup -> RSOD.

Fix: split the className argument on whitespace in deprecated.js and feed
individual tokens to classList. A small splitClasses() helper keeps the
three functions symmetrical.

Adds adversarial regression tests in test-utils.js covering:
- ASCII whitespace variants (space, tab, CR, LF, mixed runs, padding)
- Unicode whitespace (U+00A0 non-breaking space)
- de-duplication across single and multiple calls
- remove/toggle no-op on missing tokens
- toggle with status undefined / true / false
- silent no-op for whitespace-only / empty / non-string / null input
- silent no-op when the element has no classList

* Move new tests to their own file

* Add backwards-compat regression tests for deprecated.js

Locks in pre-5.4.0 tolerant behaviour of $:/core/modules/utils/
deprecated.js helpers that regressed in PR #9251. Each spec targets an
edge-case input the current one-line modern equivalents reject:

- repeat: negative count / null / undefined str
- startsWith / endsWith: RegExp search arg
- stringifyNumber: null / undefined
- domContains: boolean return, self-check
- hasClass: null element, classless element
- getLocationPath: query preservation, hash stripping
  (browser-only; pends in Node because the TW5 sandbox has no `window`)

Also picks up the addClass/removeClass/toggleClass whitespace specs
moved out of test-utils.js by the previous commit, so all deprecated.js
coverage lives together.

Fails 8 specs on current HEAD; the follow-up deprecated.js restoration
commit turns them green.

* Restore pre-5.4.0 behaviour of deprecated.js utilities

PR #9251 replaced several helpers in $:/core/modules/utils/deprecated.js
with one-line ES2017 equivalents that diverge from the originals on
edge-case inputs. Follow-up PRs fixed the most visible cases
(getLocationHash #9622, isDate #9771, addClass empty-string #9561 and
whitespace 005e17537); this commit closes the rest:

- repeat: manual loop tolerates negative count / null / undefined str
- startsWith / endsWith: substring compare tolerates RegExp search arg
- stringifyNumber: `num + ""` coercion tolerates null / undefined
- domContains: `a !== b && a.contains(b)` returns boolean, handles self
- hasClass: null-element guard, strict-false return
- getLocationPath: `toString().split("#")[0]` preserves the query
  string in permalinks (startup/story.js:214, 217) -- the most visible
  user-facing regression, causing ?lang=de etc. to silently drop.

IE-only fallbacks in Math.sign, strEndsWith, domContains, and
domMatchesSelector are removed; TW5 no longer supports IE.

Covered by the regression specs added in the previous commit.

* make comments more refined
2026-05-05 15:27:40 +01:00
Jeremy Ruston fff648b959 Exclude Claude from npm (#9847) 2026-05-05 10:28:28 +01:00
Mario Pietsch e1cff6068c Fix SelectWidget multi-select highlight regression for options containing HTML (#9839) (#9841)
PR #8093 used `child.children.length === 0` to distinguish a plain <option> from an <optgroup>. This test misfires when an <option> contains inline HTML.

E.g. tc-tiddlylink anchors auto-generated for "$:/..." titles.
So on refresh the option's `selected` state was never restored.

- This change switches to a tagName check.
- Adds a regression test plus a guard for the original PR #8092 optgroup
behaviour.
2026-05-05 10:23:36 +01:00
Mario Pietsch 6721164f15 Add a new test that fails, because a missing typeof test in fakedom.js (#9840)
* Add a new test that fails, because a missing typeof test in fakedom.js

* Add typeof strint fix to fakedom to avoid throw
2026-05-05 11:20:49 +02:00
buggyj 3c37aa267d Fix for #9834 Widgets are compiled before modules are loaded (#9835) 2026-05-05 10:16:21 +01:00
Jeremy Ruston a603146b3c Update version number to v5.4.1 2026-05-05 10:13:05 +01:00
Mario Pietsch f992f4ffab Fix 9826 min height issue (#9828)
* Add new tests

* fix 9826 minHeight handling + docs
2026-05-05 10:11:01 +01:00
Mario Pietsch dfedd8594f save impact note 9609 as .tid instead of .json (#9844) 2026-04-29 16:01:35 +02:00
Jeremy Ruston 7a80bd4a6f Serializer should preserve created and modified fields 2026-04-22 10:53:49 +01:00
Jeremy Ruston 82bde9bffc Merge branch 'master' into bidirectional-filesystem 2026-04-21 20:25:02 +01:00
Jeremy Ruston ea84baa5a3 Merge branch 'tiddlywiki-com' 2026-04-21 20:24:23 +01:00
Mario Pietsch a4e4d36bf6 Fix Typo in Link in Multi-Valued Variables (#9824) 2026-04-20 21:31:52 +01:00
Mario Pietsch 3ed481b2e2 Update the Archive and Release Notes Description (#9823) 2026-04-20 21:31:09 +01:00
Jeremy Ruston 27c60ff58d Prepare for v5.5.0 2026-04-20 20:03:12 +01:00
Jeremy Ruston 748ef8aa8d New release should be first thumbnail 2026-04-20 19:56:08 +01:00
Jeremy Ruston 9830d4338c Merge branch 'master' into bidirectional-filesystem 2026-04-19 09:41:09 +01:00
Jeremy Ruston 75267b730a Merge branch 'master' into bidirectional-filesystem 2026-04-16 15:40:00 +01:00
Jeremy Ruston 3bc78e6641 Register for text/x-markdown as well as text/markdown 2026-04-14 14:49:14 +01:00
Jeremy Ruston 765386a4f9 Fix security warning from github-advanced-security's CodeQL scan 2026-04-14 11:37:04 +01:00
Jeremy Ruston ca8dcb690a Fix Netlify build failure 2026-04-14 11:25:02 +01:00
Jeremy Ruston 99a00ab1b4 Add Markdown serializer/deserializer
Not strictly related to dynamic stores, but very useful in connection with them
2026-04-14 10:48:40 +01:00
Jeremy Ruston f95966d5bb Genuflection to Eslint 2026-04-14 09:48:45 +01:00
Jeremy Ruston 3a20837c96 Fix dynamic-store save path and add tiddlerserializer module type
For tiddlers loaded from a dynamic store, compute originalpath relative
to the store directory so save-time path resolution lands at the correct
location (previously produced URL-encoded absolute-path filenames).

Add a tiddlerserializer module type mirroring tiddlerdeserializer: when
a serializer is registered for a tiddler's content type, save as a
single self-contained file with no .meta sidecar.
2026-04-14 09:45:03 +01:00
Jeremy Ruston e3e49bb61e Try to fix hang on netlify build 2026-04-13 18:09:21 +01:00
Jeremy Ruston 748969322b Another attempt to get tests passing on Netlify 2026-04-13 17:55:58 +01:00
Jeremy Ruston 4611e3569f New release banner 2026-04-13 17:51:29 +01:00
Jeremy Ruston c1e145ca12 Attempt to fix test errors in CI 2026-04-13 17:46:17 +01:00
Jeremy Ruston 882504c8d1 Fix change note 2026-04-13 17:39:00 +01:00
Jeremy Ruston b8f542458b Initial Commit 2026-04-13 17:14:12 +01:00
151 changed files with 4649 additions and 446 deletions
+1
View File
@@ -3,3 +3,4 @@
tmp/
output/
node_modules/
.claude/
+1 -1
View File
@@ -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.4.0
TW5_BUILD_VERSION=v5.5.0.
fi
echo "Using TW5_BUILD_VERSION as [$TW5_BUILD_VERSION]"
+60 -8
View File
@@ -1539,8 +1539,8 @@ Register all the module tiddlers that have a module type
$tw.Wiki.prototype.defineShadowModules = function() {
var self = this;
this.eachShadow(function(tiddler,title) {
// Don't define the module if it is overidden by an ordinary tiddler
if(!self.tiddlerExists(title) && tiddler.hasField("module-type")) {
// Don't define the module if it is overidden by an ordinary tiddler, or has already been defined
if(!self.tiddlerExists(title) && tiddler.hasField("module-type") && !$tw.utils.hop($tw.modules.titles,title)) {
if(tiddler.hasField("draft.of")) {
// Report a fundamental problem
console.warn(`TiddlyWiki: Plugins should not contain tiddlers with a 'draft.of' field: ${tiddler.fields.title}`);
@@ -1971,7 +1971,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
});
// Helper to process a file
var processFile = function(filename,isTiddlerFile,fields,isEditableFile,rootPath) {
var processFile = function(filename,isTiddlerFile,fields,isEditableFile,rootPath,dynamicStoreId) {
var extInfo = $tw.config.fileExtensionInfo[path.extname(filename)],
type = (extInfo || {}).type || fields.type || "text/plain",
typeInfo = $tw.config.contentTypeInfo[type] || {},
@@ -2046,9 +2046,9 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
});
});
if(isEditableFile) {
tiddlers.push({filepath: pathname, hasMetaFile: !!metadata && !isTiddlerFile, isEditableFile: true, tiddlers: fileTiddlers});
tiddlers.push({filepath: pathname, hasMetaFile: !!metadata && !isTiddlerFile, isEditableFile: true, dynamicStoreId: dynamicStoreId, tiddlers: fileTiddlers});
} else {
tiddlers.push({tiddlers: fileTiddlers});
tiddlers.push({dynamicStoreId: dynamicStoreId, tiddlers: fileTiddlers});
}
};
// Helper to recursively search subdirectories
@@ -2089,6 +2089,31 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
// Process directory specifier
var dirPath = path.resolve(filepath,dirSpec.path);
if(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
// Register a dynamic store if requested
var dynamicStoreId = null;
if(dirSpec.dynamicStore && $tw.boot.dynamicStores) {
dynamicStoreId = dirPath;
var existing = null;
for(var ds=0; ds<$tw.boot.dynamicStores.length; ds++) {
if($tw.boot.dynamicStores[ds].id === dynamicStoreId) {
existing = $tw.boot.dynamicStores[ds];
break;
}
}
if(!existing) {
$tw.boot.dynamicStores.push({
id: dynamicStoreId,
directory: dirPath,
saveFilter: dirSpec.dynamicStore.saveFilter || "",
watch: dirSpec.dynamicStore.watch !== false,
debounce: dirSpec.dynamicStore.debounce || 400,
filesRegExp: dirSpec.filesRegExp || "^.*$",
searchSubdirectories: !!dirSpec.searchSubdirectories,
isTiddlerFile: !!dirSpec.isTiddlerFile,
fields: dirSpec.fields || {}
});
}
}
var files = getAllFiles(dirPath, dirSpec.searchSubdirectories),
fileRegExp = new RegExp(dirSpec.filesRegExp || "^.*$"),
metaRegExp = /^.*\.meta$/;
@@ -2097,7 +2122,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
filename = path.basename(thisPath);
if(filename !== "tiddlywiki.files" && !metaRegExp.test(filename) && fileRegExp.test(filename)) {
dirSpec.fields = dirSpec.fields || {};
processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile,dirSpec.path);
processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile || !!dirSpec.dynamicStore,dirSpec.path,dynamicStoreId);
}
}
} else {
@@ -2284,6 +2309,24 @@ $tw.loadWikiTiddlers = function(wikiPath,options) {
$tw.loadPlugins(wikiInfo.plugins,$tw.config.pluginsPath,$tw.config.pluginsEnvVar);
$tw.loadPlugins(wikiInfo.themes,$tw.config.themesPath,$tw.config.themesEnvVar);
$tw.loadPlugins(wikiInfo.languages,$tw.config.languagesPath,$tw.config.languagesEnvVar);
// Register plugin-provided tiddlerdeserializer and tiddlerserializer modules now,
// so they are available when the wiki tiddler files are read from disk below.
// We also apply the supporting `utils`, `tiddlerfield`, and `tiddlermethod`
// modules so deserializers can call into them (e.g. core's text/html
// deserializer needs $tw.utils.extractEncryptedStoreArea).
// (All of these steps run again later in execStartup; they are idempotent.)
$tw.wiki.readPluginInfo();
$tw.wiki.registerPluginTiddlers("plugin");
$tw.wiki.unpackPluginTiddlers();
$tw.wiki.defineShadowModules();
$tw.modules.applyMethods("utils",$tw.utils);
if($tw.node) {
$tw.modules.applyMethods("utils-node",$tw.utils);
}
$tw.Tiddler.fieldModules = $tw.modules.getModulesByTypeAsHashmap("tiddlerfield");
$tw.modules.applyMethods("tiddlermethod",$tw.Tiddler.prototype);
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
$tw.modules.applyMethods("tiddlerserializer",$tw.Wiki.tiddlerSerializerModules);
// Load the wiki files, registering them as writable
var resolvedWikiPath = path.resolve(wikiPath,$tw.config.wikiTiddlersSubDir);
$tw.utils.each($tw.loadTiddlersFromPath(resolvedWikiPath),function(tiddlerFile) {
@@ -2293,7 +2336,8 @@ $tw.loadWikiTiddlers = function(wikiPath,options) {
filepath: tiddlerFile.filepath,
type: tiddlerFile.type,
hasMetaFile: tiddlerFile.hasMetaFile,
isEditableFile: config["retain-original-tiddler-path"] || tiddlerFile.isEditableFile || tiddlerFile.filepath.indexOf($tw.boot.wikiTiddlersPath) !== 0
isEditableFile: config["retain-original-tiddler-path"] || tiddlerFile.isEditableFile || tiddlerFile.filepath.indexOf($tw.boot.wikiTiddlersPath) !== 0,
dynamicStoreId: tiddlerFile.dynamicStoreId || null
};
});
}
@@ -2305,7 +2349,10 @@ $tw.loadWikiTiddlers = function(wikiPath,options) {
for(var title in $tw.boot.files) {
fileInfo = $tw.boot.files[title];
if(fileInfo.isEditableFile) {
relativePath = path.relative($tw.boot.wikiTiddlersPath,fileInfo.filepath);
// For tiddlers loaded from a dynamic store, compute originalpath relative to the store's directory
// so that save-time path resolution against that directory yields the correct location.
var basePath = fileInfo.dynamicStoreId || $tw.boot.wikiTiddlersPath;
relativePath = path.relative(basePath,fileInfo.filepath);
fileInfo.originalpath = relativePath;
output[title] =
path.sep === "/" ?
@@ -2431,6 +2478,8 @@ $tw.boot.initStartup = function(options) {
if(!$tw.boot.tasks.readBrowserTiddlers) {
// For writable tiddler files, a hashmap of title to {filepath:,type:,hasMetaFile:}
$tw.boot.files = Object.create(null);
// Array of {id, directory, saveFilter, watch, debounce} registered via tiddlywiki.files dynamicStore directives
$tw.boot.dynamicStores = [];
// System paths and filenames
$tw.boot.bootPath = options.bootPath || path.dirname(module.filename);
$tw.boot.corePath = path.resolve($tw.boot.bootPath,"../core");
@@ -2520,6 +2569,9 @@ $tw.boot.initStartup = function(options) {
// Install the tiddler deserializer modules
$tw.Wiki.tiddlerDeserializerModules = Object.create(null);
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
// Install the tiddler serializer modules
$tw.Wiki.tiddlerSerializerModules = Object.create(null);
$tw.modules.applyMethods("tiddlerserializer",$tw.Wiki.tiddlerSerializerModules);
// Call unload handlers in the browser
if($tw.browser) {
window.onbeforeunload = function(event) {
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
title: TiddlyWiki Team
tags: Community/Team
modified: 20260708095630754
created: 20260708095630754
leader: @BurningTreeC
team:
The TiddlyDesktop development repository is at https://github.com/TiddlyWiki/TiddlyDesktop
+19 -2
View File
@@ -238,6 +238,10 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
// Save as a .tid file
fileInfo.type = "application/x-tiddler";
fileInfo.hasMetaFile = false;
} else if($tw.Wiki.tiddlerSerializerModules && $tw.Wiki.tiddlerSerializerModules[tiddlerType]) {
// A serializer is registered for this content type - save as a single self-contained file
fileInfo.type = tiddlerType;
fileInfo.hasMetaFile = false;
} else {
// Save as a text/binary file and a .meta file
fileInfo.type = tiddlerType;
@@ -416,7 +420,16 @@ Save a tiddler to a file described by the fileInfo:
*/
exports.saveTiddlerToFile = function(tiddler,fileInfo,callback) {
$tw.utils.createDirectory(path.dirname(fileInfo.filepath));
if(fileInfo.hasMetaFile) {
var serializer = $tw.Wiki.tiddlerSerializerModules && $tw.Wiki.tiddlerSerializerModules[fileInfo.type];
if(serializer && !fileInfo.hasMetaFile && fileInfo.type !== "application/x-tiddler" && fileInfo.type !== "application/json") {
var typeInfo = $tw.config.contentTypeInfo[fileInfo.type] || {encoding: "utf8"};
fs.writeFile(fileInfo.filepath,serializer(tiddler),typeInfo.encoding,function(err) {
if(err) {
return callback(err);
}
return callback(null,fileInfo);
});
} else 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) {
@@ -458,7 +471,11 @@ Save a tiddler to a file described by the fileInfo:
*/
exports.saveTiddlerToFileSync = function(tiddler,fileInfo) {
$tw.utils.createDirectory(path.dirname(fileInfo.filepath));
if(fileInfo.hasMetaFile) {
var serializer = $tw.Wiki.tiddlerSerializerModules && $tw.Wiki.tiddlerSerializerModules[fileInfo.type];
if(serializer && !fileInfo.hasMetaFile && fileInfo.type !== "application/x-tiddler" && fileInfo.type !== "application/json") {
var typeInfo = $tw.config.contentTypeInfo[fileInfo.type] || {encoding: "utf8"};
fs.writeFileSync(fileInfo.filepath,serializer(tiddler),typeInfo.encoding);
} else 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);
+4 -5
View File
@@ -9,15 +9,14 @@ Serve tiddlers over http
"use strict";
let fs, url, path, querystring, crypto, zlib;
let fs, path, crypto, zlib, URL;
if($tw.node) {
fs = require("fs"),
url = require("url"),
path = require("path"),
querystring = require("querystring"),
crypto = require("crypto"),
zlib = require("zlib");
URL = require("url").URL;
}
/*
@@ -260,8 +259,8 @@ Server.prototype.requestHandler = function(request,response,options) {
state.wiki = options.wiki || self.wiki;
state.boot = options.boot || self.boot;
state.server = self;
state.urlInfo = url.parse(request.url);
state.queryParameters = querystring.parse(state.urlInfo.query);
state.urlInfo = new URL(request.url, "http://localhost");
state.queryParameters = Object.fromEntries(state.urlInfo.searchParams);
state.pathPrefix = options.pathPrefix || this.get("path-prefix") || "";
// Enable CORS
if(this.corsEnable) {
+2 -1
View File
@@ -255,4 +255,5 @@ ViewTemplateTags/Caption: View Template Tags
ViewTemplateTags/Hint: This rule cascade is used by the default view template to dynamically choose the template for displaying the tags area of a tiddler.
WikiInformation/Caption: Wiki Information
WikiInformation/Hint: This page summarises high level information about the configuration of this ~TiddlyWiki. It is designed to enable users to quickly share relevant aspects of the configuration of their ~TiddlyWiki with others, for example when seeking help in one of the forums. No private or personal information is included, and nothing is shared without being explicitly copied and pasted elsewhere
WikiInformation/Drag/Caption: Drag this link to copy this tool to another wiki
WikiInformation/Drag/Caption: Drag this link to copy this tool to another wiki
WikiInformation/Generate/Caption: Click to generate wiki information report
+2 -3
View File
@@ -156,14 +156,13 @@ Fix the height of textarea to fit content
FramedEngine.prototype.fixHeight = function() {
// Make sure styles are updated
this.copyStyles();
// If .editRows is initialised, it takes precedence
if(this.widget.editTag === "textarea" && !this.widget.editRows) {
if(this.widget.editTag === "textarea") {
if(this.widget.editAutoHeight) {
if(this.domNode && !this.domNode.isTiddlyWikiFakeDom) {
var newHeight = $tw.utils.resizeTextAreaToFit(this.domNode,this.widget.editMinHeight);
this.iframeNode.style.height = newHeight + "px";
}
} else {
} else if(!this.widget.editRows) {
var fixedHeight = parseInt(this.widget.wiki.getTiddlerText(HEIGHT_VALUE_TITLE,"400px"),10);
fixedHeight = Math.max(fixedHeight,20);
this.domNode.style.height = fixedHeight + "px";
+2 -3
View File
@@ -100,13 +100,12 @@ SimpleEngine.prototype.getText = function() {
Fix the height of textarea to fit content
*/
SimpleEngine.prototype.fixHeight = function() {
// If .editRows is initialised, it takes precedence
if((this.widget.editTag === "textarea") && !this.widget.editRows) {
if(this.widget.editTag === "textarea") {
if(this.widget.editAutoHeight) {
if(this.domNode && !this.domNode.isTiddlyWikiFakeDom) {
$tw.utils.resizeTextAreaToFit(this.domNode,this.widget.editMinHeight);
}
} else {
} else if(!this.widget.editRows) {
var fixedHeight = parseInt(this.widget.wiki.getTiddlerText(HEIGHT_VALUE_TITLE,"400px"),10);
fixedHeight = Math.max(fixedHeight,20);
this.domNode.style.height = fixedHeight + "px";
+4 -5
View File
@@ -22,12 +22,11 @@ exports.tag = function(source,operator,options) {
});
} else {
// Old semantics:
var tiddlers;
if(operator.prefix === "!") {
// Returns a copy of the input if operator.operand is missing
tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
const excludeTagSet = new Set(options.wiki.getTiddlersWithTag(operator.operand));
source(function(tiddler,title) {
if(tiddlers.indexOf(title) === -1) {
if(!excludeTagSet.has(title)) {
results.push(title);
}
});
@@ -39,9 +38,9 @@ exports.tag = function(source,operator,options) {
return indexedResults;
}
} else {
tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
const includeTagSet = new Set(options.wiki.getTiddlersWithTag(operator.operand));
source(function(tiddler,title) {
if(tiddlers.indexOf(title) !== -1) {
if(includeTagSet.has(title)) {
results.push(title);
}
});
+3 -2
View File
@@ -88,10 +88,11 @@ BackSubIndexer.prototype.update = function(updateDescriptor) {
var newTargets = [],
oldTargets = [],
self = this;
if(updateDescriptor.old.exists) {
// System tiddlers are never indexed as sources, matching the _init() scan
if(updateDescriptor.old.exists && !this.wiki.isSystemTiddler(updateDescriptor.old.tiddler.fields.title)) {
oldTargets = this._getTarget(updateDescriptor.old.tiddler);
}
if(updateDescriptor.new.exists) {
if(updateDescriptor.new.exists && !this.wiki.isSystemTiddler(updateDescriptor.new.tiddler.fields.title)) {
newTargets = this._getTarget(updateDescriptor.new.tiddler);
}
+66 -54
View File
@@ -528,67 +528,79 @@ exports.parseAttribute = function(source,pos) {
pos = token.end;
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
// Look for a string literal
var stringLiteral = $tw.utils.parseStringLiteral(source,pos);
if(stringLiteral) {
pos = stringLiteral.end;
node.type = "string";
node.value = stringLiteral.value;
} else {
do {
// Look for a string literal
var stringLiteral = $tw.utils.parseStringLiteral(source,pos);
if(stringLiteral) {
pos = stringLiteral.end;
node.type = "string";
node.value = stringLiteral.value;
break;
}
// Look for a filtered value
var filteredValue = $tw.utils.parseTokenRegExp(source,pos,reFilteredValue);
if(filteredValue) {
pos = filteredValue.end;
node.type = "filtered";
node.filter = filteredValue.match[1];
} else {
// Look for an indirect value
var indirectValue = $tw.utils.parseTokenRegExp(source,pos,reIndirectValue);
if(indirectValue) {
pos = indirectValue.end;
node.type = "indirect";
node.textReference = indirectValue.match[1];
} else {
// Look for a macro invocation value
var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos);
if(macroInvocation) {
pos = macroInvocation.end;
node.type = "macro";
node.value = macroInvocation;
} else {
// Look for an MVV reference value
var mvvReference = $tw.utils.parseMVVReferenceAsTransclusion(source,pos);
if(mvvReference) {
pos = mvvReference.end;
node.type = "macro";
node.value = mvvReference;
node.isMVV = true;
} else {
var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue);
if(substitutedValue) {
pos = substitutedValue.end;
node.type = "substituted";
node.rawValue = substitutedValue.match[1] || substitutedValue.match[2];
} else {
// Look for a unquoted value
var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute);
if(unquotedValue) {
pos = unquotedValue.end;
node.type = "string";
node.value = unquotedValue.match[1];
} else if(source.charAt(pos) === "<" && source.charAt(pos + 1) === "<" && source.indexOf(">>",pos) !== -1) {
// Value looks like a macro invocation (starts with << with a closing >> ahead) but does not parse as one. Return null so the enclosing tag fails to parse rather than silently binding the attribute to "true" and treating the remainder as further attributes (restores v5.3.8 behaviour)
return null;
} else {
node.type = "string";
node.value = "true";
}
}
}
}
}
break;
}
}
// Look for an indirect value
var indirectValue = $tw.utils.parseTokenRegExp(source,pos,reIndirectValue);
if(indirectValue) {
pos = indirectValue.end;
node.type = "indirect";
node.textReference = indirectValue.match[1];
break;
}
// Look for a macro invocation value
var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos);
if(macroInvocation) {
pos = macroInvocation.end;
node.type = "macro";
node.value = macroInvocation;
break;
}
// Look for an MVV reference value
var mvvReference = $tw.utils.parseMVVReferenceAsTransclusion(source,pos);
if(mvvReference) {
pos = mvvReference.end;
node.type = "macro";
node.value = mvvReference;
node.isMVV = true;
break;
}
// Look for a substituted value
var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue);
if(substitutedValue) {
pos = substitutedValue.end;
node.type = "substituted";
node.rawValue = substitutedValue.match[1] || substitutedValue.match[2];
break;
}
// Look for a unquoted value
var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute);
if(unquotedValue) {
pos = unquotedValue.end;
node.type = "string";
node.value = unquotedValue.match[1];
break;
}
if(source.charAt(pos) === "<" && source.charAt(pos + 1) === "<" && source.indexOf(">>",pos) !== -1) {
// Value looks like a macro invocation (starts with << with a closing >> ahead) but does not parse as one. Return null so the enclosing tag fails to parse rather than silently binding the attribute to "true" and treating the remainder as further attributes (restores v5.3.8 behaviour)
return null;
}
node.type = "string";
node.value = "true";
} while(false);
} else {
// If there is no equals sign or colon, then this is an attribute with no value, defaulting to "true"
node.type = "string";
@@ -31,13 +31,16 @@ exports.parse = function() {
reEnd.lastIndex = this.parser.pos;
var match = reEnd.exec(this.parser.source),
text,
start = this.parser.pos;
start = this.parser.pos,
textEnd;
// Process the text
if(match) {
text = this.parser.source.substring(this.parser.pos,match.index);
textEnd = match.index;
this.parser.pos = match.index + match[0].length;
} else {
text = this.parser.source.substr(this.parser.pos);
textEnd = this.parser.sourceLength;
this.parser.pos = this.parser.sourceLength;
}
return [{
@@ -47,7 +50,7 @@ exports.parse = function() {
type: "text",
text: text,
start: start,
end: this.parser.pos
end: textEnd
}]
}];
};
@@ -32,7 +32,8 @@ exports.parse = function() {
this.parser.pos = this.matchRegExp.lastIndex;
// Create the link unless it is suppressed
if(this.match[0].substr(0,1) === "~") {
return [{type: "text", text: this.match[0].substr(1), start: start, end: this.parser.pos}];
// Start after the suppressing "~" so the span matches the plain text
return [{type: "text", text: this.match[0].substr(1), start: start + 1, end: this.parser.pos}];
} else {
return [{
type: "element",
@@ -34,7 +34,7 @@ exports.parse = function() {
// Parse the filter terminated by a line break
var reMatch = /(.*)(?:$|\r?\n)/mg;
reMatch.lastIndex = this.parser.pos;
var filterStart = this.parser.source;
var filterStart = this.parser.pos;
var match = reMatch.exec(this.parser.source);
this.parser.pos = reMatch.lastIndex;
// Parse tree nodes to return
@@ -27,9 +27,11 @@ Parse the most recent match
*/
exports.parse = function() {
// Get the details of the match
var linkText = this.match[0];
var linkText = this.match[0],
// Start after the suppressing "~" so the span matches the plain text
start = this.parser.pos + 1;
// Move past the wikilink
this.parser.pos = this.matchRegExp.lastIndex;
// Return the link without unwikilink character as plain text
return [{type: "text", text: linkText.substr(1)}];
return [{type: "text", text: linkText.substr(1), start: start, end: this.parser.pos}];
};
+3 -2
View File
@@ -34,12 +34,15 @@ exports.startup = function() {
$tw.modules.applyMethods("wikimethod",$tw.Wiki.prototype);
$tw.wiki.addIndexersToWiki();
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
$tw.modules.applyMethods("tiddlerserializer",$tw.Wiki.tiddlerSerializerModules);
$tw.macros = $tw.modules.getModulesByTypeAsHashmap("macro");
$tw.wiki.initParsers();
// --------------------------
// The rest of the startup process here is not strictly to do with loading modules, but are needed before other startup
// modules are executed. It is easier to put them here than to introduce a new startup module
// --------------------------
// Set up the performance framework
$tw.perf = new $tw.Performance($tw.wiki.getTiddlerText(PERFORMANCE_INSTRUMENTATION_CONFIG_TITLE,"no") === "yes");
// Create a root widget for attaching event handlers. By using it as the parentWidget for another widget tree, one can reuse the event handlers
$tw.rootWidget = new widget.widget({
type: "widget",
@@ -48,8 +51,6 @@ exports.startup = function() {
wiki: $tw.wiki,
document: $tw.browser ? document : $tw.fakeDocument
});
// Set up the performance framework
$tw.perf = new $tw.Performance($tw.wiki.getTiddlerText(PERFORMANCE_INSTRUMENTATION_CONFIG_TITLE,"no") === "yes");
// Kick off the filter tracker
$tw.filterTracker = new $tw.FilterTracker($tw.wiki);
$tw.wiki.addEventListener("change",function(changes) {
+2 -1
View File
@@ -15,7 +15,8 @@ var getCellInfo = function(text, start, length, SEPARATOR) {
var isCellQuoted = text.charAt(start) === QUOTE;
var cellStart = isCellQuoted ? start + 1 : start;
if(text.charAt(i) === SEPARATOR) {
// A quote licenses a separator inside the cell, so only an unquoted cell reads an immediate separator as empty
if(!isCellQuoted && text.charAt(cellStart) === SEPARATOR) {
return [cellStart, cellStart, false];
}
+60 -11
View File
@@ -3,18 +3,43 @@ title: $:/core/modules/utils/deprecated.js
type: application/javascript
module-type: utils
Deprecated util functions
Deprecated util functions. These preserve the pre-5.4.0 signatures and
behaviour for backwards compatibility with plugins and external scripts.
Prefer modern alternatives in new code (Array.prototype methods, classList,
Math.sign, String.prototype.repeat, etc.).
\*/
exports.logTable = (data) => console.table(data);
exports.repeat = (str,count) => str.repeat(count);
/*
Repeats a string
*/
exports.repeat = function(str,count) {
var result = "";
for(var t=0;t<count;t++) {
result += str;
}
return result;
};
exports.startsWith = (str,search) => str.startsWith(search);
/*
Check if a string starts with another string
*/
exports.startsWith = function(str,search) {
return str.substring(0, search.length) === search;
};
exports.endsWith = (str,search) => str.endsWith(search);
/*
Check if a string ends with another string
*/
exports.endsWith = function(str,search) {
return str.substring(str.length - search.length) === search;
};
/*
Trim whitespace from the start and end of a string
*/
exports.trim = function(str) {
if(typeof str === "string") {
return str.trim();
@@ -29,30 +54,54 @@ exports.sign = Math.sign;
exports.strEndsWith = (str,ending,position) => str.endsWith(ending,position);
exports.stringifyNumber = (num) => num.toString();
exports.stringifyNumber = function(num) {
return num + "";
};
// Returns the fully escaped CSS selector for a tag, e.g.
// "$:/tags/Stylesheet" -> "tc-tagged-\%24\%3A\%2Ftags\%2FStylesheet"
exports.tagToCssSelector = function(tagName) {
return "tc-tagged-" + encodeURIComponent(tagName).replace(/[!"#$%&'()*+,\-./:;<=>?@[\\\]^`{\|}~,]/mg,function(c) {
return "\\" + c;
});
};
exports.domContains = (a,b) => a.compareDocumentPosition(b) & 16;
/*
Determines whether element 'a' contains element 'b'.
Returns false when a === b (matches the original John Resig semantics).
*/
exports.domContains = function(a,b) {
return a !== b && a.contains(b);
};
exports.domMatchesSelector = (node,selector) => node.matches(selector);
exports.hasClass = (el,className) => el.classList && el.classList.contains(className);
exports.hasClass = function(el,className) {
return !!(el && el.classList && el.classList.contains(className));
};
// addClass/removeClass/toggleClass split on whitespace to preserve the
// original setAttribute("class", ...) acceptance of "foo bar" as two
// classes. Regressed in #9251.
function splitClasses(className) {
return (typeof className === "string" && className.match(/\S+/g)) || [];
}
exports.addClass = function(el,className) {
el.classList && className && el.classList.add(className);
if(!el.classList) return;
splitClasses(className).forEach(function(c) { el.classList.add(c); });
};
exports.removeClass = function(el,className) {
el.classList && className && el.classList.remove(className);
if(!el.classList) return;
splitClasses(className).forEach(function(c) { el.classList.remove(c); });
};
exports.toggleClass = function(el,className,status) {
el.classList && className && el.classList.toggle(className, status);
if(!el.classList) return;
splitClasses(className).forEach(function(c) { el.classList.toggle(c,status); });
};
exports.getLocationPath = () => window.location.origin + window.location.pathname;
exports.getLocationPath = function() {
return window.location.toString().split("#")[0];
};
+13
View File
@@ -77,10 +77,23 @@ exports.resizeTextAreaToFit = function(domNode,minHeight) {
// Measure the specified minimum height
domNode.style.height = minHeight;
var measuredHeight = domNode.offsetHeight || parseInt(minHeight,10);
// Temporarily force rows=1 during auto-measurement so the intrinsic floor
// is one row rather than the HTML default of two; restore afterwards
var hadRowsAttr = domNode.hasAttribute("rows"),
savedRows = hadRowsAttr ? domNode.getAttribute("rows") : null;
if(!hadRowsAttr) {
domNode.setAttribute("rows","1");
}
// Set its height to auto so that it snaps to the correct height
domNode.style.height = "auto";
// Calculate the revised height
var newHeight = Math.max(domNode.scrollHeight + domNode.offsetHeight - domNode.clientHeight,measuredHeight);
// Restore the original rows attribute state
if(!hadRowsAttr) {
domNode.removeAttribute("rows");
} else {
domNode.setAttribute("rows",savedRows);
}
// Only try to change the height if it has changed
if(newHeight !== domNode.offsetHeight) {
domNode.style.height = newHeight + "px";
+22 -2
View File
@@ -82,6 +82,11 @@ var TW_Style = function(el) {
// Return a Proxy to handle direct access to individual style properties
return new Proxy(styleObject, {
get: function(target, property) {
// Real CSSStyleDeclaration returns undefined for non-string keys.
// Guards against crashes when consumers probe Symbol.toPrimitive etc.
if(typeof property !== "string") {
return undefined;
}
// If the property exists on styleObject, return it (get, set, setProperty methods)
if(property in target) {
return target[property];
@@ -90,6 +95,10 @@ var TW_Style = function(el) {
return el._style[$tw.utils.convertStyleNameToPropertyName(property)] || "";
},
set: function(target, property, value) {
// Mirror the get trap: ignore non-string keys instead of crashing.
if(typeof property !== "string") {
return true;
}
// Set the property in _style
el._style[$tw.utils.convertStyleNameToPropertyName(property)] = value;
return true;
@@ -106,7 +115,9 @@ var TW_Element = function(tag, namespace) {
this.children = [];
this._style = {}; // Internal style object
this.style = new TW_Style(this); // Proxy for style management
this.namespaceURI = namespace || "http://www.w3.org/1999/xhtml";
// createElementNS with empty-string or null normalises to null (no namespace) per spec.
// https://dom.spec.whatwg.org/#dom-document-createelementns
this.namespaceURI = namespace !== undefined ? (namespace || null) : "http://www.w3.org/1999/xhtml";
};
@@ -197,7 +208,16 @@ TW_Element.prototype.addEventListener = function(type,listener,useCapture) {
Object.defineProperty(TW_Element.prototype, "tagName", {
get: function() {
return this.tag || "";
if(!this.tag) {
return "";
}
// HTML elements report uppercase tagName per DOM spec. Other namespaces
// preserve case. Fakedom only models HTML documents.
// https://dom.spec.whatwg.org/#dom-element-tagname
if(this.namespaceURI === "http://www.w3.org/1999/xhtml") {
return this.tag.toUpperCase();
}
return this.tag;
}
});
+27 -7
View File
@@ -243,8 +243,28 @@ exports.slowInSlowOut = function(t) {
};
exports.copyObjectPropertiesSafe = function(object) {
const seen = new Set(),
isDOMElement = (value) => value instanceof Node || value instanceof Window;
const seen = new Set();
function isDOMElement(value) {
if(!value || typeof value !== "object") {
return false;
}
// Cross-realm DOM nodes
if(typeof value.nodeType === "number" &&
typeof value.nodeName === "string") {
return true;
}
// Cross-realm Window objects
if(value.window === value &&
value.document &&
value.location) {
return true;
}
return false;
}
function safeCopy(obj) {
// skip circular references
@@ -255,10 +275,6 @@ exports.copyObjectPropertiesSafe = function(object) {
if(typeof obj !== "object" || obj === null) {
return obj;
}
// skip DOM elements
if(isDOMElement(obj)) {
return undefined;
}
// copy arrays, preserving positions
if(Array.isArray(obj)) {
return obj.map((item) => {
@@ -266,7 +282,11 @@ exports.copyObjectPropertiesSafe = function(object) {
return value === undefined ? null : value;
});
}
// skip DOM elements
if(isDOMElement(obj)) {
return undefined;
}
seen.add(obj);
const copy = {};
let key,
+1 -1
View File
@@ -22,7 +22,7 @@ Inherit from the base widget class
DiffTextWidget.prototype = new Widget();
DiffTextWidget.prototype.invisibleCharacters = {
"\n": "↩︎\n",
"\n": "\n",
"\r": "⇠",
"\t": "⇥\t"
};
+3 -4
View File
@@ -125,13 +125,12 @@ SelectWidget.prototype.setSelectValue = function() {
values = Array.isArray(value) ? value : $tw.utils.parseStringArray(value);
for(var i=0; i < select.children.length; i++){
child=select.children[i];
if(child.children.length === 0){
child.selected = values.indexOf(child.value) !== -1;
} else {
// grouped options
if(child.tagName && child.tagName.toUpperCase() === "OPTGROUP"){
for(var y=0; y < child.children.length; y++){
child.children[y].selected = values.indexOf(child.children[y].value) !== -1;
}
} else {
child.selected = values.indexOf(child.value) !== -1;
}
}
} else {
+5 -3
View File
@@ -4,13 +4,15 @@ description: create a new journal tiddler
\whitespace trim
\function get-tags() [<textFieldTags>] [<tagsFieldTags>] +[join[ ]]
<$let journalTitleTemplate={{$:/config/NewJournal/Title}} textFieldTags={{$:/config/NewJournal/Tags}} tagsFieldTags={{$:/config/NewJournal/Tags!!tags}} journalText={{$:/config/NewJournal/Text}}>
<$wikify name="journalTitle" text="<$transclude $variable='now' format=<<journalTitleTemplate>>/>">
<$let journalTitle=<<now format={{$:/config/NewJournal/Title}}>>
textFieldTags={{$:/config/NewJournal/Tags}}
tagsFieldTags={{$:/config/NewJournal/Tags!!tags}}
journalText={{$:/config/NewJournal/Text}}
>
<$reveal type="nomatch" state=<<journalTitle>> text="">
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=<<get-tags>> text={{{ [<journalTitle>get[]] }}}/>
</$reveal>
<$reveal type="match" state=<<journalTitle>> text="">
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=<<get-tags>> text=<<journalText>>/>
</$reveal>
</$wikify>
</$let>
+16 -23
View File
@@ -6,30 +6,23 @@ tags: $:/tags/EditTemplate
\procedure lingo-base() $:/language/EditTemplate/
\procedure tag-body-inner(colour,fallbackTarget,colourA,colourB,icon,tagField:"tags")
<$wikify name="foregroundColor"
text="""<$macrocall $name="contrastcolour"
target=<<colour>>
fallbackTarget=<<fallbackTarget>>
colourA=<<colourA>>
colourB=<<colourB>>/>
"""
>
<$let backgroundColor=<<colour>> >
<span class="tc-tag-label tc-tag-list-item tc-small-gap-right"
data-tag-title=<<currentTiddler>>
style=`color:$(foregroundColor)$; fill:$(foregroundColor)$; background-color:$(backgroundColor)$;`
<$let foregroundColor=<<contrastcolour target=<<colour>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>> >>
backgroundColor=<<colour>>
>
<span class="tc-tag-label tc-tag-list-item tc-small-gap-right"
data-tag-title=<<currentTiddler>>
style=`color:$(foregroundColor)$; fill:$(foregroundColor)$; background-color:$(backgroundColor)$;`
>
<$transclude tiddler=<<icon>>/>
<$view field="title" format="text"/>
<$button class="tc-btn-invisible tc-remove-tag-button"
style.fill=<<foregroundColor>>
>
<$transclude tiddler=<<icon>>/>
<$view field="title" format="text"/>
<$button class="tc-btn-invisible tc-remove-tag-button"
style.fill=<<foregroundColor>>
>
<$action-listops $tiddler=<<saveTiddler>> $field=<<tagField>> $subfilter="-[{!!title}]"/>
{{$:/core/images/close-button}}
</$button>
</span>
</$let>
</$wikify>
<$action-listops $tiddler=<<saveTiddler>> $field=<<tagField>> $subfilter="-[{!!title}]"/>
{{$:/core/images/close-button}}
</$button>
</span>
</$let>
\end
\procedure tag-body(colour,palette,icon,tagField:"tags")
+2 -4
View File
@@ -16,15 +16,13 @@ title: $:/core/ui/TagPickerTagTemplate
<$set name="backgroundColor"
value={{{ [<currentTiddler>] :cascade[all[shadows+tiddlers]tag[$:/tags/TiddlerColourFilter]!is[draft]get[text]] }}}
>
<$wikify name="foregroundColor"
text="""<$macrocall $name="contrastcolour" target=<<backgroundColor>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>>/>"""
>
<$let foregroundColor=<<contrastcolour target=<<backgroundColor>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>> >> >
<span class="tc-tag-label tc-btn-invisible"
style=<<tag-pill-styles>>
data-tag-title=<<currentTiddler>>
>
{{||$:/core/ui/TiddlerIcon}}<$view field="title" format="text"/>
</span>
</$wikify>
</$let>
</$set>
</$button>
+4 -3
View File
@@ -6,7 +6,6 @@ description: {{$:/language/Buttons/NewJournalHere/Hint}}
\whitespace trim
\procedure journalButton()
<$button tooltip={{$:/language/Buttons/NewJournalHere/Hint}} aria-label={{$:/language/Buttons/NewJournalHere/Caption}} class=<<tv-config-toolbar-class>>>
<$wikify name="journalTitle" text="""<$transclude $variable="now" format=<<journalTitleTemplate>>/>""">
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=`[[$(currentTiddlerTag)$]] $(journalTags)$`/>
<%if [<tv-config-toolbar-icons>match[yes]] %>
{{$:/core/images/new-journal-button}}
@@ -16,9 +15,11 @@ description: {{$:/language/Buttons/NewJournalHere/Hint}}
<$text text={{$:/language/Buttons/NewJournalHere/Caption}}/>
</span>
<%endif%>
</$wikify>
</$button>
\end
<$let journalTitleTemplate={{$:/config/NewJournal/Title}} journalTags={{$:/config/NewJournal/Tags}} currentTiddlerTag=<<currentTiddler>>>
<$let journalTitle=<<now format={{$:/config/NewJournal/Title}}>>
journalTags={{$:/config/NewJournal/Tags}}
currentTiddlerTag=<<currentTiddler>>
>
<<journalButton>>
</$let>
+5 -1
View File
@@ -17,6 +17,10 @@ This page summarises high level information about the configuration of this ~Tid
Drag this link to copy this tool to another wiki
\end intrinsic-lingo-Drag/Caption
\procedure intrinsic-lingo-Generate/Caption()
Click to generate wiki information report
\end intrinsic-lingo-Generate/Caption
\procedure lingo(title,mode:"inline")
<%if [<title>addprefix<lingo-base>is[shadow]] %>
<$transclude $tiddler={{{ [<title>addprefix<lingo-base>] }}} $mode=<<mode>>/>
@@ -105,7 +109,7 @@ Drag this link to copy this tool to another wiki
<$button>
<<display-wiki-info-modal>>
Click to generate wiki information report
<<lingo title:"Generate/Caption">>
</$button>
<$link to="$:/core/ui/ControlPanel/WikiInformation">
+1 -1
View File
@@ -1,6 +1,6 @@
title: $:/config/OfficialPluginLibrary
tags: $:/tags/PluginLibrary
url: https://tiddlywiki.com/library/v5.4.0/index.html
url: https://tiddlywiki.com/library/v5.5.0/index.html
caption: {{$:/language/OfficialPluginLibrary}}
{{$:/language/OfficialPluginLibrary/Hint}}
@@ -8,9 +8,8 @@ type: text/vnd.tiddlywiki
# Ensure the new release banner image is up to date
# Update ''master'' with changes from ''tiddlywiki-com''
# Verify the version numbers in [[$:/config/OfficialPluginLibrary]] in `core/wiki/config/OfficialPluginLibrary.tid`
# Move the latest release note from the prerelease edition into the tw5.com edition
# Adjust the release date and the ''released'' field of the latest release tiddler (eg, [[Release 5.1.3]])
# Also adjust the github.com comparison link to point to the tag for the new release
# Adjust the release date and the ''released'' field of the release tiddler (eg, [[Release 5.1.3]])
# Update the ''release-introduction'' definition with the new release text, and if necessary the ''description'' field of the release tiddler
# Adjust the tiddler [[TiddlyWiki Archive]] to include the new version number
# Ensure [[TiddlyWiki Releases]] has the new version as the default tab
# Adjust the modified time of HelloThere
@@ -23,7 +22,6 @@ type: text/vnd.tiddlywiki
# Run `./bin/readme-bld.sh` to build the readme files
# Commit the new readme files to ''master''
# Restore `package.json` to the previous version number
# Adjust the link for "GitHub for detailed change history of this release" in the release note
# Add the credits for the new release banner to the release note, including a link to the GitHub instance of the image from the commit history
!! Make New Release
-1
View File
@@ -2,7 +2,6 @@
"description": "Developer documentation from https://tiddlywiki.com/dev/",
"plugins": [
"tiddlywiki/highlight",
"tiddlywiki/nodewebkitsaver",
"tiddlywiki/github-fork-ribbon",
"tiddlywiki/menubar",
"tiddlywiki/internals",
-1
View File
@@ -1,7 +1,6 @@
{
"description": "A step by step introduction to TiddlyWiki",
"plugins": [
"tiddlywiki/cecily",
"tiddlywiki/codemirror",
"tiddlywiki/highlight",
"tiddlywiki/katex"
@@ -11,6 +11,7 @@ ja-title: TiddlyWikiアーカイブ
5.1.20 5.1.21 5.1.22 5.1.23
5.2.0 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7
5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.3.8
5.4.0
\end
TiddlyWikiの古いバージョンは[[アーカイブ|https://github.com/TiddlyWiki/tiddlywiki.com-gh-pages/tree/master/archive]]で入手できます:
@@ -21,3 +21,5 @@ type: text/vnd.tiddlywiki
<<.tip """上に示したように、図に開始線と終了線が1つある場合は、リンクされた上位レベルにさらに情報があることを意味します。パンくずリストはナビゲーションに使用できます""">>
<<.tip """下位レベルで使用されているように、図に開始点と終了点がない場合は、読みやすさと単純さを高めるために、上位レベルのシンタックス要素が削除されていることを意味します。パンくずリストはナビゲーションに使用できます""">>
<<.note """フィルタ式の再帰深度は最大300です。この制限を超えて自身を再帰的に呼び出すフィルタ(例えば、<<.olink subfilter>>や<<.olink filter>>演算子を使ったもの)は、エラーメッセージ`/**-- Excessive filter recursion --**/`を返します。これは無限ループを防ぐためです。""">>
@@ -14,6 +14,8 @@ type: text/vnd.tiddlywiki
"{" [: <-"間接"-> /"}以外"/] "}"
|
"<" [: <-"変数"-> /">以外"/] ">"
|
"(" [: <-"複数値変数"-> /")以外"/ ] ")" /"v5.4.0"/
)
"""/>
@@ -22,6 +24,7 @@ type: text/vnd.tiddlywiki
;<<.def ハード>>
: `[パラメータ例]`
: パラメータは、角括弧内のテキストそのものです。
;<<.def ソフト>>
: <<.def 間接>>
:: `{パラメータ例}`
@@ -30,6 +33,9 @@ type: text/vnd.tiddlywiki
:: `<パラメータ例>`
:: パラメータは、山括弧内の[[変数|Variables]]の現在値です。マクロパラメータは、v5.2.0まではサポートされて<<.em いません>>。
::<<.from-version "5.2.0">> リテラルマクロパラメータがサポートされています。例: `[<now [UTC]YYYY0MM0DD0hh0mm0ssXXX>]`。
: <<.def "複数値変数">>
:: `(パラメータ例)`
:: <<.from-version "5.4.0">> このパラメータは、丸括弧で囲まれた名前の[[複数値変数|Multi-Valued Variables]]に格納されている値のリスト全体に展開されます。このようにアクセスすると、フィルタ演算子は最初の値だけでなく、すべての値を受け取ります。詳細については、[[複数値変数|Multi-Valued Variables]]を参照してください。
<<.note """すべての[[フィルタオペレータ|filter Operator]]の後にはパラメータ式が続く必要があります。[[パラメータの無いオペレータ|Operators without parameters]]の場合、その式は空になります(`[<currentTiddler>links[]]`のフィルタオペレータ<<.olink links>>と同様)。""">>
@@ -37,4 +43,8 @@ type: text/vnd.tiddlywiki
<<.from-version "5.1.23">> [[フィルタステップ|Filter Step]]では、`,`文字で区切られた複数のパラメータがサポートされます。
例えば: `[param1],[param2]`や`<param1>,{param2}`
例えば: `[param1],[param2]`や`<param1>,{param2}`、 `[param1],(param2)`
---
<<.warning """`/regexp/(flags)`オペランド構文は非推奨であり、ブラウザのコンソールに警告が表示されます。代わりに<<.olink regexp>>演算子を使用してください。""">>
@@ -24,13 +24,13 @@ type: text/vnd.tiddlywiki
多くのステップでは、ステップの実行内容をさらに定義する明示的な<<.def パラメータ>>が必要です。
<<.def サフィックス>>は、特定のオペレータの意味を拡張する追加テキスト(多くの場合、[[フィールド|TiddlerFields]]名)です。
<<.def サフィックス>>は、特定のオペレータの意味を拡張する追加テキスト(多くの場合、[[フィールド|TiddlerFields]]名)です。サフィックスは`:`文字で区切られ、各サフィックスグループには、カンマで区切られた複数の値を含めることができます。例えば、`compare:number:gteq`は、最初のサフィックスは`number`で、2番目のサフィックスは`gteq`です。`:sort:string:reverse,casesensitive`は、最初のサフィックスは`string`で、2番目のサフィックスグループには`reverse`と`casesensitive`の両方が含まれます。
ステップの<<.def オペレータ>>と<<.def サフィックス>>がすべて省略されている場合は、デフォルトで[[title|title Operator]]オペレータが使用されます。
ステップの<<.def オペレータ>>と<<.def サフィックス>>がすべて//省略//されている場合は、デフォルトで[[title|title Operator]]オペレータが使用されます。サフィックスが存在するが、コロンの前のオペレータ名が空の場合(例: `[:fieldname[value]]`)、オペレータはデフォルトで<<.olink field>>になります。
<<.from-version "5.1.23">> いくつかのステップでは、`,`文字で区切られた複数の<<.def パラメータ>>を受け入れます。
認識されないオペレータは、<<.olink field>>オペレータのサフィックスであるかのように扱われます。
認識されないオペレータは、<<.olink field>>オペレータのサフィックスであるかのように扱われます。<<.from-version "5.3.0">> ただし、名前に`.`(ドット)文字が含まれる認識されないオペレータは、ユーザー定義のフィルタオペレータとして扱われ、その名前の変数または関数を検索することで解決されます。.
フィルタオペレータはプラグインによって拡張できます。
@@ -16,6 +16,7 @@ type: text/vnd.tiddlywiki
|`-[run]` |`:except[run]` |差集合 |... AND NOT ラン |
|`~[run]` |`:else[run]` |それ以外 |... ELSE ラン |
|`=[run]` |`:all[run]` |重複を排除しない和集合 |... OR ラン |
|`=>[run]` |`:let[run]` |<<.from-version "5.4.0">> 結果を変数に割りあてる |... LET ラン |
ランのインプットは通常、Wiki内の[[隠し|ShadowTiddlers]]Tiddler以外のすべてのTiddlerタイトルのリストです(順不同)。<br>ただし、`+`プレフィックスによってこれを変更できます:
@@ -45,3 +46,5 @@ type: text/vnd.tiddlywiki
新しいフィルタランプレフィックスを作成するには、`filterrunprefix`の[[モジュールタイプ|ModuleType]]で
[[Javascriptモジュール|Modules]]を作成します。
コンパイルされたフィルタ式はパフォーマンスのためキャッシュされます。キャッシュには最大2000件のエントリが保存され、この制限を超えるとキャッシュ全体がリセットされます。
@@ -17,6 +17,15 @@ type: text/vnd.tiddlywiki
[["ラン"|"Filter Run"]]
"""/>
前のランからのフィルタアウトプットは保留されます。`:intersection`フィルタランは、すべてのTiddlerタイトルをインプットとして開始されます。この最後のフィルタランが完了すると、最後のアウトプットが保留アウトプットと比較されます。保留アウトプットと最新アウトプットの両方に表われるタイトルのみを含む新しいアウトプットが生成されます。
前のランからのフィルタアウトプットは保留されます。`:intersection`フィルタランは、すべてのTiddlerタイトルをインプットとして開始されます。この最後のフィルタランが完了すると、最後のアウトプットが保留アウトプットと比較されます。保留アウトプットと最新アウトプットの両方に表われるタイトルのみを含む新しいアウトプットが生成されます。アウトプットにおけるタイトルの順序は、蓄積された結果から保持されます。
!! `:and` / `+`との違い
`:intersection`と`:and` (または `+`)の主な違いは、''インプット''として何を受け取るかです:
* `:and` / `+`は、''これまでに蓄積された結果''をフィルタランへのインプットとして渡します。ラン内のオペレータは、結果に既に含まれているタイトルのみを表示・操作できます。
* `:intersection`は、''すべてのTiddlerタイトル''をフィルタランへのインプットとして渡し(プレフィックスなしのランと同様)、その後、蓄積された結果にも含まれるタイトルのみを保持します。
これは、`:intersection`がTiddlerプール全体から開始する必要のあるオペレータを使用してタイトルを照合できることを意味します。詳細については、[[交換可能なフィルタランプレフィックス|Interchangeable Filter Run Prefixes]]を参照してください。
[[intersectionフィルタランプレフィックス(例)|Intersection Filter Run Prefix (Examples)]]
@@ -0,0 +1,15 @@
created: 20260222152853141
modified: 20260506104906199
original-modified: 20260222184916224
tags: [[Let Filter Run Prefix]]
title: Let Filter Run Prefix (Examples)
ja-title: letフィルタランプレフィックス(例)
type: text/vnd.tiddlywiki
<<.operator-example 1 "3 2 1 4 :let[[myvar]] 6 7 8 [(myvar)sort[]]" "名前付きプレフィックスを使用">>
<<.operator-example 2 "3 2 1 4 =>myvar 6 7 8 [(myvar)sort[]]" "短縮形のプレフィックスを使用">>
<<.operator-example 3 "3 2 1 4 =>myvar 6 7 8 [<myvar>]" "山括弧は最初の値のみを返却">>
<<.operator-example 4 "3 2 1 4 =>mynumbers [(mynumbers)sum[]] [(mynumbers)average[]]">>
<<.operator-example 5 '"[0-9]" =>digitsRE abc 123 de45 67fg hij :filter[regexp<digitsRE>]' "角括弧で囲まれた正規表現では、変数パラメータを使用">>
<<.operator-example 6 '"[prefix[ca]then[ca]]" "[suffix[at]then[at]]" other =>myfilters cat can bat bug :cascade[(myfilters)]' "[[cascadeフィルタランプレフィックス|Cascade Filter Run Prefix]]で使用するフィルタを定義">>
<<.operator-example 7 "[[ ]] [[⁑ ]] [[⁂ ]] :let[[prefixList]] [tag[Learning]first[3]] :map:flat[(prefixList)addsuffix<currentTiddler>]">>
@@ -0,0 +1,32 @@
created: 20250307212252946
from-version: 5.4.0
modified: 20260502101033176
original-modified: 20250307212252946
rp-input: 前回のフィルタランからのすべてのタイトル
rp-output: "let"フィルタランプレフィックスからはいつも空のタイトルリストが返されます
rp-purpose: 前回のフィルタラン結果のタイトルリストを複数値変数へ代入
tags: [[Named Filter Run Prefix]]
title: Let Filter Run Prefix
ja-title: letフィルタランプレフィックス
type: text/vnd.tiddlywiki
<$railroad text="""
\start none
\end none
( ":let" )
[["ラン"|"Filter Run"]]
"""/>
`:let`フィルタランプレフィックスは、前回のフィルタランの結果として得られたタイトルリストを[[他数値変数|Multi-Valued Variables]]に割り当てます。この変数には、フィルタランによって返された最初の結果が名前として付けられます。
この変数は、[[フィルタ式|Filter Expression]]内の残りの[[フィルタラン|Filter Run]]で使用可能になります。通常の方法で変数にアクセスすると、結果リストの最初の項目のみが返されます(結果リストが空の場合は空の文字列が返されます)。オペランドとして変数名を山括弧ではなく丸括弧で囲むと、結果リスト内のすべての項目が取得できます。
このプレフィックスには、オプションの[[ショートカット構文|Shortcut Filter Run Prefix]]記号`=>run`があります。例:
```
=[<myfun1>] =[<myfun2>] =>myvar
```
`:let`フィルタランプレフィックスを指定すると、常に現在の結果リストがクリアされます。
[[letフィルタランプレフィックス(例)|Let Filter Run Prefix (Examples)]]
@@ -25,6 +25,7 @@ type: text/vnd.tiddlywiki
[[<":or"> |"Or Filter Run Prefix"]] |
[[<":reduce"> |"Reduce Filter Run Prefix"]] |
[[<":sort"> /"v5.2.0"/ |"Sort Filter Run Prefix"]] |
[[<":let"> /"v5.4.0"/ |"Let Filter Run Prefix"]] |
[[<":then"> /"v5.3.0"/ |"Then Filter Run Prefix"]]) [[run|"Filter Run"]]
"""/>
@@ -35,4 +36,14 @@ type: text/vnd.tiddlywiki
<<.tip """フィルタランプレフィックス`:reduce`、`:sort`、`:map`、`:filter`内では、変数<<.var currentTiddler>>は処理中のTiddlerのタイトルに設定されます。<br>サブフィルタ外のcurrentTiddlerの値は、変数<<.var "..currentTiddler">>で使用できます <<.from-version "5.2.0">>""" >>
!! サフィックス
名前付きフィルタランプレフィックスは、`:`文字で区切られたサフィックスを受け入れることができ、各サフィックスにはオプションでカンマ区切りの値を含めることができます。一般的な構文は`:prefixname:suffix1:suffix2,...`です。現在、次のプレフィックスがサフィックスを受け入れます:
|!プレフィックス |!サフィッス |!例 |
|`:map` |`flat` — 項目ごとに最初の結果だけでなく、すべての結果を返す |`:map:flat[...]` |
|`:sort` |型 (''string'', ''alphanumeric'', ''number'', ''integer'', ''version'', ''date'')とフラグ (''reverse'', ''casesensitive'', ''caseinsensitive'') |`:sort:number:reverse[...]` |
その他の名前付きプレフィックス(`:all`, `:and`, `:cascade`, `:else`, `:except`, `:filter`, `:intersection`, `:let`, `:or`, `:reduce`, `:then`)は現在サフィックスを受け付けません。
参照: [[交換可能なフィルターランプレフィックス|Interchangeable Filter Run Prefixes]]
@@ -11,7 +11,7 @@ type: text/vnd.tiddlywiki
<$railroad text="""
\start none
\end none
(-|:"+"|"-"|"~"|"=")
(-|:"+"|"-"|"~"|"="|"=>")
[["ラン"|"Filter Run"]]
"""/>
@@ -23,8 +23,10 @@ type: text/vnd.tiddlywiki
* プレフィックス`-`は、アウトプットタイトルがフィルタのアウトプットから<<.em 取り除か>>れます(そのようなTiddlerが存在する場合)
* プレフィックス`~`は、フィルタアウトプットが空リストの場合、ランの結果のタイトルがフィルタアウトプットに[[優先的に追加|Dominant Append]]されます。フィルタアウトプットが空リストでない場合、ランは無視されます。<<.from-version "5.1.18">>
* プレフィックス`~`<<.from-version "5.1.18">>は、フィルタアウトプットが空リストの場合、ランの結果のタイトルがフィルタアウトプットに[[優先的に追加|Dominant Append]]されます。フィルタアウトプットが空リストでない場合、ランは無視されます。
* プレフィックス`=`は、アウトプットタイトルが重複排除されずにフィルタのアウトプットに追加されます。<<.from-version "5.1.20">>
* プレフィックス`=`<<.from-version "5.1.20">>は、アウトプットタイトルが重複排除されずにフィルタのアウトプットに追加されます。
* プレフィックス`=>` <<.from-version "5.4.0">>は、これまでのすべてのランの累積結果は、このランの最初の結果が名前となる[[複数値変数|Multi-Valued Variables]]として割り当てられます。その後、累積結果はクリアされます。
{{Interchangeable Filter Run Prefixes}}
@@ -2,7 +2,7 @@ created: 20210618133745003
from-version: 5.3.0
modified: 20250423104147235
original-modified: 20230710074225410
rp-input: <<.olink すべて>>のTiddlerタイトル
rp-input: <<.olink all>>のTiddlerタイトル (前回のランのアウトプットが空でない場合にのみ評価されます)
rp-output: フィルタランのアウトプットは、空のリストでない限り、前回までのランのアウトプットを置き換えます(以下を参照)。
rp-purpose: フィルタランへのインプットをそのアウトプットで置き換え、インプットがある場合にのみランを評価
search:
@@ -20,8 +20,9 @@ type: text/vnd.tiddlywiki
関数はいくつかの方法で呼び出すことができます:
* 構文 `<<myfun param:"value">>` を使用して関数を直接トランスクルードする
* 構文 `<div class=<<myfun param:"value">>>` を使用してウィジェット属性に関数を割り当て
* [[Calls]]構文を使用する:
** 構文 `<<myfun param:"value">>` を使用して関数を直接トランスクルードす
** 構文 `<div class=<<myfun param:"value">>>` を使用してウィジェット属性に関数を割り当てる
* 構文 `[function[myfun],[value],...]` を使用して [[関数オペレータ|function Operator]] を介して関数を呼び出す
* 構文 `[my.fun[value]]` または `[.myfun[value]]` で、カスタムフィルター演算子として名前にピリオドが含まれる関数を直接呼び出す
@@ -1,13 +0,0 @@
caption: Internet Explorer
created: 20140811172058274
modified: 20241029105938082
original-modified: 20211114031651879
tags: GettingStarted $:/deprecated
title: GettingStarted - Internet Explorer
ja-title: はじめに - Internet Explorer
type: text/vnd.tiddlywiki
{{Saving with TiddlyIE}}
The [[Windows HTA Hack]] describes an alternative method of using TiddlyWiki with Internet Explorer.
[[Windows HTAのハック|Windows HTA Hack]]では、Internet ExplorerでTiddlyWikiを使用する別の方法について説明します。
@@ -1,8 +1,8 @@
created: 20130822170200000
icon: $:/core/icon
list: [[A Gentle Guide to TiddlyWiki]] [[Discover TiddlyWiki]] [[Some of the things you can do with TiddlyWiki]] [[Ten reasons to switch to TiddlyWiki]] Examples [[What happened to the original TiddlyWiki?]]
modified: 20251213104120189
original-modified: 20250807084952911
modified: 20260430010447598
original-modified: 20260420192600833
tags: Welcome
title: HelloThere
ja-title: こんにちは
@@ -1,5 +1,5 @@
created: 20150414070451144
list: list: [[HelloThumbnail - Donations]] [[HelloThumbnail - Newsletter]] [[HelloThumbnail - Community Survey 2025]] [[HelloThumbnail - Introduction Video]] [[HelloThumbnail - Grok TiddlyWiki]] [[HelloThumbnail - Latest Version]] [[HelloThumbnail - MultiWikiServer]] [[HelloThumbnail - Twenty Years of TiddlyWiki]] [[HelloThumbnail - TiddlyWiki Privacy]] [[HelloThumbnail - Marketplace]] [[HelloThumbnail - Intertwingled Innovations]] [[HelloThumbnail - TiddlyWikiLinks]]
list: [[HelloThumbnail - Latest Version]] [[HelloThumbnail - Donations]] [[HelloThumbnail - Newsletter]] [[HelloThumbnail - Community Survey 2025]] [[HelloThumbnail - Introduction Video]] [[HelloThumbnail - Grok TiddlyWiki]] [[HelloThumbnail - MultiWikiServer]] [[HelloThumbnail - Twenty Years of TiddlyWiki]] [[HelloThumbnail - TiddlyWiki Privacy]] [[HelloThumbnail - Marketplace]] [[HelloThumbnail - Intertwingled Innovations]] [[HelloThumbnail - TiddlyWikiLinks]]
modified: 20260406043045066
original-modified: 20150414070948246
title: HelloThumbnail
@@ -5,6 +5,7 @@ caption: v<<version>>の新着情報
link: Releases
image: New Release Banner
color: #fff
ribbon-text: NEW
\define prerelease-regexp() [0-9]+\.[0-9]+\.[0-9]+\-prerelease
<$list filter="[<version>!regexp<prerelease-regexp>]" variable="ignore">
@@ -6,6 +6,5 @@ link: TiddlyWiki Newsletter
image: TiddlyWiki Newsletter Badge
color: #fff
type: text/vnd.tiddlywiki
ribbon-text: NEW
~TiddlyWikiコミュニティからの最も興味深く関連性のあるニュースをまとめた~TiddlyWikiニュースレターを購読できます
@@ -1,19 +0,0 @@
caption: HTA Hack
color: #F06292
created: 20131212223146250
delivery: DIY
description: Internet Explorerで変更を手動で直接保存する方法
method: save
modified: 20241012122755089
original-modified: 20200507110355115
tags: Saving Windows $:/deprecated
title: Windows HTA Hack
ja-title: Windows HTAのハック
type: text/vnd.tiddlywiki
<<.deprecated-since "5.3.6">>
Windowsでは、HTMLファイルの拡張子を`*.hta`に名前変更することで、TiddlyWikiを真のローカルアプリケーションに変換できます。その後、''fsosaver''モジュールは~ActiveX ~FileSystemObjectを使用して変更を保存できます。
この方法の欠点の1つは、TiddlyWikiファイルがUTF-16フォーマットで保存されるため、通常のUTF-8エンコードの場合と比べて2倍の大きさになることです。ただし、別の保存方法でファイルを開いて保存すると、ファイルはUTF-8に再エンコードされます。
詳細については、Wikipediaを参照してください: https://en.wikipedia.org/wiki/HTML_Application
@@ -1,58 +1,9 @@
caption: プロシージャ呼び出し
created: 20221007130006705
modified: 20260327114525571
original-modified: 20230419103154329
tags: WikiText Procedures
modified: 20260429105903754
original-modified: 20260125212303316
title: Procedure Calls
ja-title: プロシージャ呼び出し
type: text/vnd.tiddlywiki
!! 紹介
このTiddlerでは、[[プロシージャ|Procedures]]を呼び出すさまざまな方法について説明します。
!! トランスクルージョンショートカットでのプロシージャ呼び出し
[[プロシージャ|Procedures]]を呼び出すには、プロシージャ名とパラメータ値を`<<`二重の山括弧で囲みます`>>`。
```
<<my-procedure param:"これはパラメータ値です">>
```
デフォルトでは、パラメータはプロシージャの定義と同じ順序でリストされます。パラメータに名前とコロンを付けることで、異なる順序でリストすることができます。
パラメータに値が指定されていない場合は、[[プロシージャ定義|Procedure Definitions]]でそのパラメータに指定されているデフォルト値が使用されます。(デフォルト値が定義されていない場合は、パラメータは空白になります。)
各パラメータ値は、`'`シングルクォーテーション`'`、`"`ダブルクォーテーション`"`、`"""`三重のダブルクォーテーション`"""`、`[[`二重の角括弧`]]`で囲むことができます。三重のダブルクォーテーションを使用すると、値にほぼあらゆる文字列を含めることができます。値にスペース、シングルクォーテーション、ダブルクォーテーションが含まれていない場合は、区切り文字は不要です。
[[パーサーモード|WikiText parser mode: macro examples]]に関する議論を参照してください。
!! <<.wlink TranscludeWidget>>ウィジェットでのプロシージャ呼び出し
ショートカット構文は、トランスクルードするプロシージャの名前を指定する`$variable`属性を持つ<<.wlink TranscludeWidget>>ウィジェットに展開されます。
```
<$transclude $variable="my-procedure" param="これはパラメータ値です"/>
```
ウィジェットは、ショートカット構文よりも柔軟性が高く、動的なパラメータ値を指定することも可能です。
!! 属性値にプロシージャ呼び出しを割り当て
プロシージャテキストは、ウィジェットやHTML要素の属性に直接割り当てることができます。プロシージャの結果はWiki化されないため、[[パラメータ処理|Procedure Parameter Handling]]は行われません。
```
<div class=<<myclasses>>>
...
</div>
```
!! フィルタでのプロシージャ呼び出しの使用
プロシージャ呼び出しはフィルタ内で使用できます。テキストはWiki化されないため、パラメータは無視されます。
```
<$list filter="[<my-procedure>]">
...
</$list>
```
<<.deprecated-since "5.4.0" "Calls">>
@@ -0,0 +1,66 @@
caption: 呼び出し
created: 20221007130006705
modified: 20260429224543285
original-modified: 20260301030947969
tags: WikiText Procedures Functions Macros
title: Calls
ja-title: 呼び出し
type: text/vnd.tiddlywiki
!! 紹介
このTiddlerでは、[[プロシージャ|Procedures]]、[[関数|Functions]]、[[マクロ|Macros]]を呼び出すさまざまな方法について説明します。構文の正式な説明については、[[呼び出し構文|Call Syntax]]を参照してください。
!! トランスクルージョンショートカットでの呼び出し
呼び出しを実行するには、呼び出し先の名前とパラメータ値を`<<`二重の山括弧で囲みます`>>`。
```
<<my-procedure param:"これはパラメータ値です">>
```
デフォルトでは、パラメータは定義と同じ順序で解釈されます。パラメータ値に名前と等号を付けることで、異なる順序でリストすることができます。
パラメータに値が指定されていない場合は、[[プロシージャ定義|Procedure Definitions]]、[[関数定義|Function Definitions]]、[[マクロ定義|Macro Definitions]]でそのパラメータに指定されているデフォルト値が使用されます。(デフォルト値が定義されていない場合は、パラメータは空白になります。)
各パラメータ値は、`'`シングルクォーテーション`'`、`"`ダブルクォーテーション`"`、`"""`三重のダブルクォーテーション`"""`、`[[`二重の角括弧`]]`で囲むことができます。三重のダブルクォーテーションを使用すると、値にほぼあらゆる文字列を含めることができます。値にスペース、シングルクォーテーション、ダブルクォーテーションが含まれていない場合は、区切り文字は不要です。シングルクォーテーション、または、三重のバッククォーテーションで囲まれた[[置換属性値|Substituted Attribute Values]]もサポートされています。
[[パーサーモード|WikiText parser mode: macro examples]]に関する議論を参照してください。
!!! 例
<<testcase TestCases/Calls/ProcedureStaticAttributes>>
<<testcase TestCases/Calls/ProcedureDynamicAttributes>>
!! <<.wlink TranscludeWidget>>ウィジェットを用いた呼び出し
ショートカット構文は、トランスクルードするプロシージャの名前を指定する`$variable`属性を持つ<<.wlink TranscludeWidget>>ウィジェットに展開されます。
```
<$transclude $variable="my-procedure" param="これはパラメータ値です"/>
```
ウィジェットは、ショートカット構文よりも柔軟性が高く、カスタムウィジェットで上書きすることも可能です。
!! 呼び出し結果を属性値に割り当てる
呼び出しから返されるテキストは、ウィジェットやHTML要素の属性に直接割り当てることができます。呼び出しの結果はWiki化されないため、[[パラメータ処理|Procedure Parameter Handling]]は行われません。
```
<div class=<<myclasses>>>
...
</div>
```
!! フィルタでの呼び出しの使用
呼び出しはフィルタ内で使用できます。テキストはWiki化されないため、パラメータは無視されます。
//現在サポートされているのはリテラル文字列パラメータのみであることに注意してください//
```
<$list filter="[<my-procedure>]">
...
</$list>
```
@@ -1,12 +1,12 @@
created: 20130825160900000
modified: 20251214105948181
original-modified: 20241106165307259
modified: 20260430074718157
original-modified: 20250617140259415
tags: Features [[Working with TiddlyWiki]]
title: Encryption
ja-title: 暗号化
type: text/vnd.tiddlywiki
TiddlyWiki5を単一のHTMLファイルとして使用すると、[[Stanford JavaScript Crypto Library]]を使用してCCMモードのAES 128ビット暗号化でコンテンツを暗号化できます。
TiddlyWiki5を単一のHTMLファイルとして使用すると、[[Stanford JavaScript Crypto Library]]を使用してCCMモードのAES 256ビット暗号化でコンテンツを暗号化できます。
# サイドバーの''ツール''タブに切り替えて、南京錠アイコンのボタンを探します
# ボタンに<<.icon $:/core/images/unlocked-padlock>> ''パスワードの設定''と表示されている場合、現在のウィキは暗号化されていません。ボタンをクリックすると、以降の保存を暗号化するために使用されるパスワードの入力を求められます
@@ -16,5 +16,5 @@ TiddlyWiki5を単一のHTMLファイルとして使用すると、[[Stanford Jav
TiddlyWikiには、パスワード/暗号化に関連する、2つの無関係な機能があることに注意してください:
* TiddlySpotに保存するときにパスワードを設定する機能。これは、''コントロールパネル'' <<.icon $:/core/images/options-button>>の"保存"タブで行います。
* [[Node.js|TiddlyWiki on Node.js]]のサーバ構成で標準のHTTP基本認証を使用する機能。これは、ServerCommandを使用してコマンドラインで実行されます。SSLと組み合わせると、GoogleやDropboxなどのオンラインサービスで得られるのと同じレベルの暗号化転送が実現されますが、ディスク上のデータは暗号化されません
* [[Tiddlyhost]]に保存するときにパスワードを設定する機能。これは、''コントロールパネル'' <<.icon $:/core/images/options-button>>の"保存"タブで行います。
* [[Node.js|TiddlyWiki on Node.js]]のサーバ構成で標準のHTTP基本認証を使用する機能。これは、[[Listenコマンド|ListenCommand]]を使用してコマンドラインで実行されます。SSLと組み合わせると、GoogleやDropboxなどのオンラインサービスで得られるのと同じレベルの暗号化転送が実現されますが、ディスク上のデータは暗号化されません
@@ -1,28 +0,0 @@
caption: ~TiddlyIE
color: #4DB6AC
community-author: David Jade
created: 20131211220000000
delivery: Browser Extension
description: Internet Explorer用のブラウザ拡張
method: save
modified: 20241009114650356
original-modified: 20200507201415232
tags: [[Internet Explorer]] Saving $:/deprecated
title: Saving with TiddlyIE
ja-title: TiddlyIEでの保存
type: text/vnd.tiddlywiki
<<.deprecated-since "5.3.6">>
# TiddlyIEアドオンを次の場所からインストールします:
#* https://github.com/davidjade/TiddlyIE/releases
# Internet Explorerを再起動します。IEはTiddlyIEアドオンを有効にするように要求します。
#> //Microsoft Script Runtime//を有効にするプロンプトが表示される場合もあります。
# 次のリンクを[[ダウンロード|Download]]し、空のTiddlyWikiを保存します:
#> https://tiddlywiki.com/empty.html
# ダウンロードしたファイルを見つけます
#* ファイル名を変更することもできますが、拡張子`.html`か`.htm`はそのままとしてください
# Internet Explorerでファイルを開きます
# サイドバーの''新しいTiddlerを作成します'' <<.icon $:/core/images/new-button>>ボタンを使用して、新しいTiddlerを作成してみてください。Tiddlerのコンテンツを入力し、<<.icon $:/core/images/done-button>> ''編集内容を確定します''ボタンをクリックします
# サイドバーの<<.icon $:/core/images/save-button-dynamic>> ''Wikiを保存します''ボタンをクリックして変更を保存します。Internet Explorerは、''名前を付けて保存''ダイアログを表示して、ファイルをローカルに保存することに同意するかどうかを確認します。
# ブラウザウィンドウを更新して、変更が正しく保存されたことを確認してください
@@ -3,11 +3,13 @@ modified: 20260404110322627
original-modified: 20240619210723396
tags: [[Variable Usage]]
title: Behaviour of variables invoked via widget attributes
ja-title:
ja-title: ウィジェット属性を介して呼び出される変数の動作
type: text/vnd.tiddlywiki
|tc-first-col-min-width|k
|!宣言方法|!動作|
|\define|本文テキストに対してパラメータのテキスト置換が実行されます。それ以上の処理は行われません。テキスト置換後の結果が属性の値として使用されます|
|<<.wlink SetWidget>>, <<.wlink LetWidget>>, <<.wlink VarsWidget>>, \procedure, \widget|本文はそのまま取得され、属性の値として使用されます|
|\function|ファンクション(例: .myfun)が`<div class=<<.myfun>>/>`で呼び出されると、`<div class={{{[function[.myfun]]}}}/>`の同義になります。他のフィルタ付きトランスクルージョン(つまり、三重中括弧)と同様に、最初の結果以外はすべて破棄されます。最初の結果が属性の値として使用されます。この形式で呼び出された場合でも、ファンクションは再帰的に処理されることに注意してください。つまり、ファンクション内のフィルタ式は別のファンクションを呼び出すことができ、処理は継続されます|
|\function|ファンクション(例: .myfun)が`<div class=<<.myfun>>/>`で呼び出されると、`<div class={{{[function[.myfun]]}}}/>`の同義になります。他のフィルタ付きトランスクルージョン(つまり、三重中括弧)と同様に、最初の結果以外はすべて破棄されます。最初の結果が属性の値として使用されます。この形式で呼び出された場合でも、ファンクションは再帰的に処理されることに注意してください。つまり、ファンクション内のフィルタ式は別のファンクションを呼び出すことができ、処理は継続されます|
<<.from-version "5.4.0">> `<<var>>`の代わりに[[複数値変数の属性|Multi-Valued Variable Attribute Values]]構文`((var))`を使用すると、最初の値だけでなく、値のリスト全体が属性に渡されます。これは主に、TranscludeWidgetを介して[[複数値変数|Multi-Valued Variables]]をプロシージャや関数のパラメーターに渡す場合に便利です。
@@ -1,16 +1,46 @@
caption: 説明リスト
created: 20131205160424246
modified: 20260221103246354
original-modified: 20251229110936191
modified: 20260409111112126
original-modified: 20260405082055301
tags: WikiText
title: Description Lists in WikiText
ja-title: Wikiテキストでの説明リスト
type: text/vnd.tiddlywiki
!! 基本的な構文
HTML説明リスト(<abbr title="またの名を">別名</abbr> 定義リスト)は次の構文で作成できます:
<<wikitext-example src:"; 説明する用語
: その用語の説明/定義
; 別の用語
: 別の説明/定義
">>
">>
!! 複数の用語と説明
1つの用語に対して複数の説明を作成することも、1つの説明に対して複数の用語を作成することもできます:
<<wikitext-example src:"; ねずみ
: 体が小さく尾が長い齧歯類
: コンピュータを操作するために使用するポインティングデバイス
; りんご
; なし
: バラ科に属する果物の一種
">>
!! 入れ子になった説明リスト
説明リストはネストして、リストの中にリストを作成することもできます:
<<wikitext-example src:"; コーヒー
: 焙煎したコーヒー豆から作られた飲み物
:; ブラックコーヒー
:: 添加物を一切使用しないコーヒー
:; ミルクコーヒー
:: コーヒーにスチームミルクやフォームミルクを加えたもの
::; ラテ
::: エスプレッソとスチームミルクで作ったコーヒー
; ティー
: 一般的に茶葉から作られる飲み物
">>
@@ -40,7 +40,7 @@ type: text/vnd.tiddlywiki
|`__下線テキスト__`には二重アンダースコアを使用します|`<u>下線テキスト<u>` |
|`^^上付き文字^^`テキストには二重サーカムフレックスアクセントを使用します |`<sup>上付き文字</sup>`テキストには二重サーカムフレックスアクセントを使用します |
|`,,下付き文字,,`テキストには二重カンマを使用します |`<sub>下付き文字</sub>`テキストには二重カンマを使用します |
|`~~取り消し線~~`テキストには二重チルダ記号を使用します |`<strike>取り消し線</strike>`テキストには二重チルダ記号を使用します |
|`~~取り消し線~~`テキストには二重チルダ記号を使用します |`<s>取り消し線</s>`テキストには二重チルダ記号を使用します |
|```等幅文字` ``には単一のバッククォートを使用します |`<code>等幅文字</code>`には単一のバッククォートを使用します |
|`@@ハイライト@@`するには二重@記号を使用します |`<span class="tc-inline-style">ハイライト</span>`するには二重@記号を使用します |
@@ -1,7 +1,7 @@
caption: リスト
created: 20131205160257619
modified: 20260225114001626
original-modified: 20160607093103220
modified: 20260409112505342
original-modified: 20260405094604397
tags: WikiText Lists
title: Lists in WikiText
ja-title: Wikiテキストでのリスト
@@ -45,6 +45,10 @@ type: text/vnd.tiddlywiki
#** そしてもう一つ
">>
! 説明リスト
{{Description Lists in WikiText}}
! CSSクラス
次の表記法を使用して、リストの個々のメンバーにCSSクラスを割り当てることもできます:
@@ -2,59 +2,8 @@ caption: マクロ呼び出し
created: 20150220182252000
modified: 20260321104805874
original-modified: 20230419103154328
tags: WikiText Macros
title: Macro Calls
ja-title: マクロ呼び出し
type: text/vnd.tiddlywiki
!! 紹介
このTiddlerでは、[[マクロ|Macros]]を呼び出すさまざまな方法について説明します。
!! マクロ呼び出しトランスクルージョンのショートカット
[[マクロ|Macros]]を呼び出すには、マクロ名とパラメータ値を`<<`二重山括弧`>>`で囲みます。
```
<<mymacro param:"これはパラメータ値です">>
```
デフォルトでは、パラメータはマクロの定義と同じ順序でリストします。パラメータに名前とコロンを付けることで、異なる順序でリストすることも可能です。
パラメータに値が指定されていない場合は、[[マクロ定義|Macro Definitions]]でそのパラメータに指定されているデフォルト値が代わりに使用されます。(デフォルト値が定義されていない場合は、パラメータはブランクになります。)
各パラメータ値は、`'`シングルクォーテーション`'`、`"`ダブルクォーテーション`"`、`"""`三重ダブルクォーテーション`"""`、または`[[`二重角括弧`]]`で囲むことができます。三重ダブルクォーテーションを使用すると、値にほぼあらゆる文字列を含めることができます。値にスペース、シングルクォーテーション、ダブルクォーテーションが含まれていない場合は、区切り文字は必要ありません。
この構文の正式な[[説明|Macro Call Syntax]]も利用可能です。
[[パーサーモード|WikiText parser mode: macro examples]]に関する[[例|Macro Calls in WikiText (Examples)]]と議論を参照してください。
!! <<.wlink TranscludeWidget>>ウィジェットを用いたマクロ呼び出し
ショートカット構文は、トランスクルードするマクロの名前を指定する`$variable`属性を持つ<<.wlink TranscludeWidget>>ウィジェットに展開されます。
```
<$transclude $variable="mymacro" param="これはパラメータ値です"/>
```
ウィジェット自体は、パラメータ値を指定できるなど、ショートカット構文よりも柔軟です。
!! 属性値にマクロ呼び出しを割り当てる
マクロの実行結果は、ウィジェットやHTML要素の属性に割り当てることができます。マクロの実行結果はWiki変換されませんが、[[パラメータの置換|Macro Parameter Handling]]は実行されます。
```
<div class=<<myclasses "Horizontal">>>
...
</div>
```
!! フィルターでマクロ呼び出しを使用する
マクロ呼び出しはフィルタ内で使用できます:
```
<$list filter="[<mymacro param:'value'>]">
...
</$list>
```
<<.deprecated-since "5.4.0" "Calls">>
@@ -0,0 +1,39 @@
created: 20250208120000000
modified: 20260507110511201
original-modified: 20250208120000000
tags: WikiText [[Widget Attributes]]
title: Multi-Valued Variable Attribute Values
ja-title: 複数値変数の属性値
type: text/vnd.tiddlywiki
<<.from-version "5.4.0">> 複数値変数の属性値は、変数名を二重の丸括弧で囲むことで指定できるようになりました。これにより、[[複数値変数|Multi-Valued Variables]]の最初の値だけでなく、すべての値が属性に渡されます。
```
<$transclude $variable="myproc" items=((myvar))/>
```
これは、下位互換性のために最初の値のみを返す[[変数の属性値|Variable Attribute Values]]構文`<<var>>`の複数値版です。この関係は、`<var>`が単一の値を返し、`(var)`がすべての値を返すフィルタオペランドの既存の慣例を反映しています。
! 複数値変数非対応の属性
`((var))`が複数値変数をサポートしていないウィジェット属性([[textウィジェット|TextWidget]]の`text`属性など)で使用される場合、最初の値のみが使用されます:
```
<$text text=((myvar))/>
```
! プロシージャに複数値変数を渡す
この構文の主なユースケースは、複数の値を持つ変数を`$transclude`パイプラインを通してプロシージャや関数に渡すことです:
```
\procedure showItems(itemList)
<$text text={{{ [(itemList)join[-]] }}}/>
\end
<$let items={{{ [all[tiddlers]sort[]] }}}>
<$transclude $variable="showItems" itemList=((items))/>
</$let>
```
この例では、`items`に格納されているTiddlerタイトルの完全なリストが`showItems`プロシージャの`itemList`パラメータに渡されます。
@@ -1,5 +1,5 @@
created: 20220817153236691
modified: 20260404112841923
modified: 20260409114243582
original-modified: 20221010074314452
tags: [[Tables in WikiText]]
title: Tables in WikiText CSS Utility Classes
@@ -81,3 +81,14 @@ full-tiddler-widthである表に、左右の余白を追加するには、`tc-m
| セル1|<$edit-text tiddler="$:/temp/test-table-input" tag="input" field="test"/> |
|^ [[Tiddlerへのリンク|Link to a tiddler]]<br>さらに詳しい説明|<$edit-text tiddler="$:/temp/test-table-input" field="text"/> |
""">>
!! 行スタイルを交互にした表
独自のスタイルシートを作成する必要があります。[[スタイルシートの使用|Using Stylesheets]]を参照してください。考え方は、`:nth-of-type(even/odd)`を使用してルールを作成することです。例:
```
.myclass tbody tr:nth-of-type(even) { background-color: <<color tiddler-editor-fields-even>>; }
.myclass tbody tr:nth-of-type(odd) { background-color: <<color tiddler-editor-fields-odd>>; }
```
<<.note """~TiddlyWikiは、テーブルの行に`evenRow`クラスと`oddRow`クラスを自動的に適用します。ただし、これらのクラスの使用は推奨されません。行は'even'(想定とは逆)で始まり、ヘッダー、フッター、本文のいずれに表示されるかに関わらず、テーブルのすべての行は単一の結合された行セットとして扱われます。""">>
@@ -66,6 +66,28 @@ or, when used with a template, `{{{ [tag[mechanism]]||TemplateTitle }}}` expands
<<.tip "//Internals//プラグインをインストールすると、生成されたウィジェットツリーをエディターのプレビューペインに表示できます">>
!! 複数値変数の表示
<<.from-version "5.4.0">> `((var))`構文をインラインで使用して、[[複数値変数|Multi-Valued Variables]]の値を、デフォルトではカンマとスペースで連結して表示できるようになりました:
```
((myvar))
((myvar||:))
```
オプションの`||`区切り文字は、カスタムの区切り文字を指定します。
!! インラインフィルター表示
<<.from-version "5.4.0">> `(((filter)))`構文によりフィルタ式の結果をデフォルトではカンマとスペースで連結してインラインで表示します:
```
((( [all[tiddlers]sort[]] )))
((( [all[tiddlers]sort[]] ||: )))
```
オプションの`||`区切り文字は、カスタムの区切り文字を指定します。これは、フィルタリングされたトランスクルージョン構文`{{{ }}}`に対応するインライン表示の表現です。
---
参照:
@@ -13,6 +13,7 @@ type: text/vnd.tiddlywiki
* [[マクロ/変数のトランスクルージョン|Variable Attribute Values]]
* [[フィルター式の結果として|Filtered Attribute Values]]
* <<.from-version "5.3.0">> [[指定された文字列に対してフィルタと変数置換を実行した結果として|Substituted Attribute Values]]
* <<.from-version "5.4.0">> [[複数値変数の参照として|Multi-Valued Variable Attribute Values]]
|属性タイプ|構文|h
|リテラル |スペースを含まない値には、シングルクォーテーション、ダブルクォーテーション、トリプルクォーテーション、またはクォーテーションなし |
@@ -20,9 +21,9 @@ type: text/vnd.tiddlywiki
|変数 |マクロまたは変数呼び出しを囲む二重の山括弧 |
|フィルタリング結果 |フィルター式を囲む三重の中括弧|
|置換結果|置換処理を行うテキストの、単一または三重のバッククォート|
|複数値変数 |変数名を囲む二重の丸括弧 |
<$list filter="[[Literal Attribute Values]] [[Transcluded Attribute Values]] [[Variable Attribute Values]] [[Filtered Attribute Values]] [[Substituted Attribute Values]]">
<$list filter="[[Literal Attribute Values]] [[Transcluded Attribute Values]] [[Variable Attribute Values]] [[Filtered Attribute Values]] [[Substituted Attribute Values]] [[Multi-Valued Variable Attribute Values]]">
<$link><h1><$view tiddler=<<currentTiddler>> field=ja-title/></h1></$link>
<$transclude mode="block"/>
</$list>
@@ -1,6 +1,6 @@
title: $:/config/LocalPluginLibrary
tags: $:/tags/PluginLibrary
url: http://127.0.0.1:8080/prerelease/library/v5.4.0/index.html
url: http://127.0.0.1:8080/prerelease/library/v5.5.0/index.html
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease Local)
A locally installed version of the official ~TiddlyWiki plugin library at tiddlywiki.com for testing and debugging. //Requires a local web server to share the library//
@@ -1,6 +1,6 @@
title: $:/config/OfficialPluginLibrary
tags: $:/tags/PluginLibrary
url: https://tiddlywiki.com/prerelease/library/v5.4.0/index.html
url: https://tiddlywiki.com/prerelease/library/v5.5.0/index.html
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease)
The prerelease version of the official ~TiddlyWiki plugin library at tiddlywiki.com. Plugins, themes and language packs are maintained by the core team.
@@ -0,0 +1,94 @@
created: 20260714055110474
modified: 20260714055129225
tags:
title: TiddlyWiki Pre-release Size Comparison
\function bytes.to.mib(size) [abs[]divide[1048576]fixed[2]addsuffix[ MiB]]
\function bytes.to.kib(size) [abs[]divide[1024]fixed[2]addsuffix[ KiB]]
\function format.file.size(size) [<size>abs[]compare:number:gteq[1048576]bytes.to.mib<size>] :else[<size>abs[]compare:number:gteq[1024]bytes.to.kib<size>] :else[<size>addsuffix[ bytes]]
\procedure get-wiki-filesize(filepath)
\procedure completion-get-json()
<!-- Success -->
<$list filter="[<status>compare:number:gteq[200]compare:number:lteq[299]]" variable="ignore">
<$action-log msg="completed" size={{{ [<data>jsonget[size]] }}}/>
<$action-setfield $tiddler=`$:/temp/file-size-comparison/$(filepath)$` text={{{ [<data>jsonget[size]] }}} />
</$list>
\end completion-get-json
<$action-sendmessage
$message="tm-http-request"
url=`https://api.github.com/repos/TiddlyWiki/tiddlywiki.com-gh-pages/contents/$(filepath)$?ref=master`
method="GET"
oncompletion=<<completion-get-json>>
var-filepath=<<filepath>>
/>
\end get-wiki-filesize
\procedure get-wiki-filesizes()
<$list filter="empty.html prerelease/empty.html" variable="filepath">
<$action-log />
<<$transclude $variable="get-wiki-filesize" filepath=<<filepath>> >>
</$list>
\end get-wiki-filesizes
<%if [[$:/temp/file-size-comparison/empty.html]is[tiddler]] [[$:/temp/file-size-comparison/prerelease/empty.html]is[tiddler]] :and[count[]match[2]]%>
<$let delta={{{ [{$:/temp/file-size-comparison/empty.html}subtract{$:/temp/file-size-comparison/prerelease/empty.html}] }}}
message={{{ [<delta>sign[]match[-1]then[Size increased]] :else[<delta>sign[]match[1]then[Size decreased]] :else[[Size has not changed]] }}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="158" height="20" role="img" style.zoom="1.5" aria-label="<$text text=<<message>>/>: <$text text={{{ [format.file.size<delta>] }}} /> bytes">
<title><$text text=<<message>>/>: <$text text={{{ [format.file.size<delta>] }}} /></title>
<filter id="blur">
<feGaussianBlur stdDeviation="16"/>
</filter>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r">
<rect width="158" height="20" rx="3"/>
</clipPath>
<g clip-path="url(#r)">
<rect width="93" height="20" fill="#555"/>
<rect x="93" width="65" height="20" fill="#67ac09"/>
<rect width="158" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
<g transform="scale(.1)">
<g aria-hidden="true" fill="#010101">
<text x="475" y="150" fill-opacity=".8" filter="url(#blur)" textLength="830"><$text text=<<message>>/></text>
<text x="475" y="150" fill-opacity=".3" textLength="830"><$text text=<<message>>/></text>
</g>
<text x="475" y="140" textLength="830"><$text text=<<message>>/></text>
</g>
<g transform="scale(.1)">
<g aria-hidden="true" fill="#010101">
<text x="1245" y="150" fill-opacity=".8" filter="url(#blur)" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
<text x="1245" y="150" fill-opacity=".3" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
</g>
<text x="1245" y="140" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
</g>
</g>
</svg>
; Pre-release empty.html
: <$text text={{{ [{$:/temp/file-size-comparison/prerelease/empty.html}] }}} /> bytes
; <$text text={{{ [tag[ReleaseNotes]sort[released]last[]] }}} /> empty.html
: <$text text={{{ [{$:/temp/file-size-comparison/empty.html}] }}} /> bytes
---
</$let>
<%else %>
<$button actions=<<get-wiki-filesizes>> class="tc-btn-big-green">
Compare size of empty.html
</$button>
<%endif%>
@@ -13,6 +13,8 @@ The pre-release is also available as an [[empty wiki|https://tiddlywiki.com/prer
</div>
{{ TiddlyWiki Pre-release Size Comparison }}
<$list filter="[tag[ReleaseNotes]!has[released]!sort[created]]">
<div class="tc-titlebar">
<h2 class="tc-title"><$text text=<<currentTiddler>>/></h2>
+1 -1
View File
@@ -7,7 +7,7 @@ const crossPlatformIndexPath = indexPath.replace(/^\/+/, "");
test("get started link", async ({ page }) => {
// The tests can take a while to run
const timeout = 1000 * 30;
const timeout = 1000 * 60;
test.setTimeout(timeout);
// Load the generated test TW html
+71
View File
@@ -0,0 +1,71 @@
/*
Fast test runner that boots the test edition and runs selected test specs.
Skips the expensive --rendertiddler step from --build index.
Usage:
node test-parser-quick.js [test-files...]
Examples:
node test-parser-quick.js # Run ALL specs
node test-parser-quick.js test-wikitext-parser # Run one spec file
node test-parser-quick.js test-wikitext-parser test-filters # Run multiple spec files
*/
"use strict";
var $tw = require("../../boot/boot.js").TiddlyWiki();
$tw.boot.argv = ["editions/test"];
// Disable startup modules that aren't needed for tests
$tw.boot.disabledStartupModules = [
"favicon", "password", "browser-messaging", "info",
"render", "rootwidget", "story", "windows"
];
$tw.boot.boot(function() {
var args = process.argv.slice(2);
var allTests = $tw.wiki.filterTiddlers("[all[tiddlers+shadows]type[application/javascript]tag[$:/tags/test-spec]]");
// Filter test tiddlers if arguments provided
var testsToRun;
if(args.length > 0) {
testsToRun = allTests.filter(function(title) {
return args.some(function(arg) {
return title.toLowerCase().indexOf(arg.toLowerCase()) !== -1;
});
});
if(testsToRun.length === 0) {
console.error("No test files matched: " + args.join(", "));
console.error("Available test files:");
allTests.forEach(function(t) { console.error(" " + t); });
process.exit(1);
}
} else {
testsToRun = allTests;
}
// Override the test filter to only include our selected tests
var titlesSet = Object.create(null);
testsToRun.forEach(function(t) { titlesSet[t] = true; });
var origFilterTiddlers = $tw.wiki.filterTiddlers.bind($tw.wiki);
$tw.wiki.filterTiddlers = function(filterString) {
var result = origFilterTiddlers.apply(null, arguments);
if(filterString.indexOf("$:/tags/test-spec") !== -1) {
return result.filter(function(t) { return titlesSet[t]; });
}
return result;
};
console.log("Running " + testsToRun.length + " of " + allTests.length + " test files");
// Use the jasmine plugin's own runTests function
var jasmine = $tw.modules.execute("$:/plugins/tiddlywiki/jasmine/jasmine-plugin.js");
jasmine.runTests(function(err) {
if(err) {
console.error(err);
process.exit(1);
}
});
});
+4
View File
@@ -0,0 +1,4 @@
modified: 20260413092032887
title: TabFour
Text tab 4
+5
View File
@@ -0,0 +1,5 @@
caption: t 1
modified: 20260413092032887
title: TabOne
Text tab 1
+6
View File
@@ -0,0 +1,6 @@
caption: t 3
description: desc
modified: 20260413092032887
title: TabThree
Text tab 3
+5
View File
@@ -0,0 +1,5 @@
caption: t 2
modified: 20260413092032887
title: TabTwo
Text tab 2
+7
View File
@@ -0,0 +1,7 @@
code-body: yes
modified: 20260413092032887
title: body-template
!! <<currentTab>>
<$transclude tiddler=<<currentTab>> mode="block"/>
@@ -0,0 +1,5 @@
code-body: yes
modified: 20260413092032887
title: button-template
<$transclude tiddler=<<currentTab>> field="description"><$transclude tiddler=<<currentTab>> field="caption"><$macrocall $name="currentTab" $type="text/plain" $output="text/plain"/></$transclude></$transclude>
@@ -0,0 +1,72 @@
code-body: yes
modified: 20260413092032887
title: tabs-macro-definition
\define tabs-button()
\whitespace trim
<$button
set=<<tabsState>>
setTo=<<currentTab>>
default=<<__default__>>
selectedClass="tc-tab-selected"
selectedAria="aria-selected"
tooltip={{!!tooltip}}
role="tab"
data-tab-title=<<currentTab>>
>
<$tiddler tiddler=<<save-currentTiddler>>>
<$set name="tv-wikilinks" value="no">
<$transclude tiddler=<<__buttonTemplate__>> mode="inline">
<$transclude tiddler=<<currentTab>> field="caption">
<$macrocall $name="currentTab" $type="text/plain" $output="text/plain"/>
</$transclude>
</$transclude>
</$set>
</$tiddler>
<<__actions__>>
</$button>
\end
\define tabs-tab()
\whitespace trim
<$set name="save-currentTiddler" value=<<currentTiddler>>>
<$tiddler tiddler=<<currentTab>>>
<<tabs-button>>
</$tiddler>
</$set>
\end
\define tabs-tab-list()
\whitespace trim
<$list filter=<<__tabsList__>> variable="currentTab" storyview="pop">
<<tabs-tab>>
</$list>
\end
\define tabs-tab-body()
\whitespace trim
<$list filter=<<__tabsList__>> variable="currentTab">
<$reveal type="match" state=<<tabsState>> text=<<currentTab>> default=<<__default__>> retain=<<__retain__>> tag="div">
<$transclude tiddler=<<__template__>> mode="block">
<$transclude tiddler=<<currentTab>> mode="block"/>
</$transclude>
</$reveal>
</$list>
\end
\define tabs(tabsList,default,state:"$:/state/tab",class,template,buttonTemplate,retain,actions,explicitState)
\whitespace trim
<$qualify title=<<__state__>> name="qualifiedState">
<$let tabsState={{{ [<__explicitState__>minlength[1]] ~[<qualifiedState>] }}}>
<div class={{{ [[tc-tab-set]addsuffix[ ]addsuffix<__class__>] }}} role="tablist">
<div class={{{ [[tc-tab-buttons]addsuffix[ ]addsuffix<__class__>] }}}>
<<tabs-tab-list>>
</div>
<div class={{{ [[tc-tab-divider]addsuffix[ ]addsuffix<__class__>] }}}/>
<div class={{{ [[tc-tab-content]addsuffix[ ]addsuffix<__class__>] }}} role="tabpanel">
<<tabs-tab-body>>
</div>
</div>
</$let>
</$qualify>
\end
@@ -0,0 +1,5 @@
modified: 20260413092032887
title: test-tabs-horizontal-all
\import [[tabs-macro-definition]]
<<tabs "TabOne TabTwo TabThree TabFour" "TabTwo" "$:/state/test-tab-01" template:"body-template" buttonTemplate:"button-template" explicitState:"$:/state/explicit">>
@@ -0,0 +1,5 @@
modified: 20260413092032887
title: test-tabs-horizontal
\import [[tabs-macro-definition]]
<<tabs "TabOne TabTwo TabThree TabFour" "TabTwo" "$:/state/test-tab-01">>
@@ -0,0 +1,5 @@
modified: 20260413092032887
title: test-tabs-vertical
\import [[tabs-macro-definition]]
<<tabs "TabOne TabTwo TabThree TabFour" "TabTwo" "$:/state/test-tab-02" "tc-vertical">>
@@ -0,0 +1,162 @@
/*\
title: test-back-indexer.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Regression tests for #9916: the back-indexer must never record system
tiddlers as backlink or backtransclude sources, neither when the index is
first built nor when it is incrementally updated.
\*/
"use strict";
describe("Back-indexer system source tests (#9916)", function() {
function setupWiki() {
// Create a wiki with indexers and one primed backlink pair
var wiki = new $tw.Wiki();
wiki.addIndexersToWiki();
wiki.addTiddler({
title: "TestIncoming",
text: ""});
wiki.addTiddler({
title: "TestOutgoing",
text: "A link to [[TestIncoming]]"});
return wiki;
}
it("should never report a system tiddler as a backlink source", function() {
// Browser console: $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") to prime the index,
// then $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"}))
// and run the filter again; $:/temp/demo must not appear.
var wiki = setupWiki();
// The first lookup builds the lazy index; its initial scan skips system tiddlers
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
// The incremental update() must skip them too
wiki.addTiddler({
title: "$:/temp/system-source",
text: "A link to [[TestIncoming]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
});
it("should keep backlinks stable while a linking system tiddler is modified and deleted", function() {
// Browser console: $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") to prime the index,
// then $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"})),
// change its text, $tw.wiki.deleteTiddler("$:/temp/demo"); the filter result never changes.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "$:/temp/system-source",
text: "A link to [[TestIncoming]]"});
// Modify: both the old and the new side of the index update are system tiddlers
wiki.addTiddler({
title: "$:/temp/system-source",
text: "Links to [[TestIncoming]] and [[TestOutgoing]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.deleteTiddler("$:/temp/system-source");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
});
it("should drop the backlink when its source is renamed to a system title", function() {
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "Demo", text: "[[HelloThere]]"})),
// confirm Demo is in $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]"), then
// $tw.wiki.renameTiddler("Demo","$:/Demo"); neither Demo nor $:/Demo remains a source.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.renameTiddler("TestOutgoing","$:/TestOutgoing");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("");
});
it("should gain the backlink when a linking system tiddler is renamed to a normal title", function() {
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"})),
// then $tw.wiki.renameTiddler("$:/temp/demo","DemoVisible");
// DemoVisible now appears in $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]").
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "$:/temp/system-source",
text: "A link to [[TestIncoming]]"});
wiki.renameTiddler("$:/temp/system-source","VisibleSource");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,VisibleSource");
});
it("should still report normal sources for a system tiddler target", function() {
// Only sources are filtered, targets are not. Browser console:
// $tw.wiki.addTiddler(new $tw.Tiddler({title: "Demo", text: "[[$:/config/NewJournal/Tags]]"}));
// $tw.wiki.filterTiddlers("[[$:/config/NewJournal/Tags]backlinks[]]") contains Demo.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "TestSystemLinker",
text: "A link to [[$:/config/Target]]"});
expect(wiki.filterTiddlers("[[$:/config/Target]backlinks[]]").join(",")).toBe("TestSystemLinker");
});
describe("Adversarial probes", function() {
it("should never index a shadow tiddler as a source, even when revealed by deleting its override", function() {
// Goes red when a refactor of BackSubIndexer.update() checks the tiddler instead of
// the exists flag:
// if(updateDescriptor["new"].tiddler) { ... } // broken: goes red here
// if(updateDescriptor["new"].exists) { ... } // correct: stays green
// After deleteTiddler() on an override, boot.js fills new.tiddler via getTiddler(),
// which falls back to the revealed shadow, while new.exists stays false because
// only the real store counts. The broken variant indexes the shadow's links and
// the final expect fails with "TestOutgoing,ShadowSource".
// The first expect also goes red if _init() ever starts scanning shadows; the
// middle expect pins that a real override IS indexed, so this probe cannot be
// satisfied by indexing nothing at all.
// Browser console: override a plugin shadow with text [[HelloThere]], check it appears in
// $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]"), then $tw.wiki.deleteTiddler(title);
// the title disappears from the filter result even though the shadow still renders.
var wiki = setupWiki();
wiki.addTiddler({
title: "$:/plugins/test/shadow-plugin",
type: "application/json",
"plugin-type": "plugin",
text: JSON.stringify({tiddlers: {
"ShadowSource": {title: "ShadowSource", text: "A shadow link to [[TestIncoming]]"}
}})});
wiki.readPluginInfo();
wiki.registerPluginTiddlers("plugin");
wiki.unpackPluginTiddlers();
// The initial scan sees only real tiddlers, not the shadow
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "ShadowSource",
text: "An overriding link to [[TestIncoming]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,ShadowSource");
// Deleting the override reveals the shadow; it must not enter the index
wiki.deleteTiddler("ShadowSource");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
});
it("should handle hostile titles like __proto__ as source and target", function() {
// Goes red when any title-keyed hashmap on the backlinks path is created like this:
// this.index = {}; // broken: goes red here
// instead of:
// this.index = Object.create(null); // correct: stays green
// this.index = new Map(); // a Map/Set refactor also stays green
// The same applies to the boot tiddler store and to the per-target source maps
// (self.index[target] = ...). On a plain {} the assignment index["__proto__"] = x
// stores no key; it silently replaces the object's prototype. Depending on which
// map regresses, the __proto__ tiddler never registers as a source (second expect),
// or its target entry lands in the shared prototype, where it pollutes every other
// lookup and lookup("__proto__") returns garbage (third expect).
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "__proto__",
// text: "[[HelloThere]]"})); __proto__ appears in
// $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") and
// $tw.wiki.filterTiddlers("[[__proto__]backlinks[]]") lists tiddlers linking to it.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "__proto__",
text: "A link to [[TestIncoming]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,__proto__");
wiki.addTiddler({
title: "ProtoLinker",
text: "A link to [[__proto__]]"});
expect(wiki.filterTiddlers("[[__proto__]backlinks[]]").join(",")).toBe("ProtoLinker");
});
});
});
+41
View File
@@ -0,0 +1,41 @@
/*\
title: test-csv.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests the CSV parser.
\*/
"use strict";
describe("CSV tests", function() {
it("parses a simple table", function() {
expect($tw.utils.parseCsvString("a,b,c\n1,2,3")).toEqual([["a","b","c"],["1","2","3"]]);
});
it("parses a table whose first cell reads empty", function() {
expect($tw.utils.parseCsvString(",b,c\n1,2,3")).toEqual([["","b","c"],["1","2","3"]]);
});
it("parses an empty cell in the middle of a row", function() {
expect($tw.utils.parseCsvString("a,,c")).toEqual([["a","","c"]]);
});
it("parses quoted cells", function() {
expect($tw.utils.parseCsvString('a,"b,still b",c')).toEqual([["a","b,still b","c"]]);
});
it("parses a quoted cell holding an escaped quote", function() {
expect($tw.utils.parseCsvString('a,"b ""quoted""",c')).toEqual([["a",'b "quoted"',"c"]]);
});
it("honours a custom separator", function() {
expect($tw.utils.parseCsvString("a;b;c",{separator: ";"})).toEqual([["a","b","c"]]);
});
it("parses a table with a header row into hashmaps", function() {
expect($tw.utils.parseCsvStringWithHeader("a,b\n1,2")).toEqual([{a: "1", b: "2"}]);
});
});
@@ -0,0 +1,252 @@
/*\
title: test-deprecated.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Regression-guard tests for $:/core/modules/utils/deprecated.js.
Locks in pre-5.4.0 tolerant behaviour of $tw.utils helpers that regressed
in PR #9251 (one-line modern equivalents diverge on edge-case inputs).
Without the companion restoration patch to core/modules/utils/deprecated.js:
8 specs fail. With the patch: green.
The addClass/removeClass/toggleClass specs at the end were moved from
test-utils.js — RSOD guard for the SampleWizard report (class field
"aaa bbb" crashing classList.add with InvalidCharacterError).
\*/
"use strict";
describe("deprecated.js — backwards-compat",function() {
describe("$tw.utils.repeat",function() {
it("returns '' for zero or negative count (does not throw)",function() {
expect($tw.utils.repeat("x",0)).toBe("");
expect($tw.utils.repeat("x",-1)).toBe("");
expect($tw.utils.repeat("x",-100)).toBe("");
});
it("coerces null/undefined str rather than throwing",function() {
expect($tw.utils.repeat(null,3)).toBe("nullnullnull");
expect($tw.utils.repeat(undefined,2)).toBe("undefinedundefined");
});
it("still works for normal inputs",function() {
expect($tw.utils.repeat("ab",3)).toBe("ababab");
expect($tw.utils.repeat("-",5)).toBe("-----");
});
});
describe("$tw.utils.startsWith / $tw.utils.endsWith",function() {
it("tolerate a RegExp search argument without throwing",function() {
// Old impl uses substring coercion; native String.prototype.startsWith
// throws TypeError when passed a RegExp.
expect(function() { $tw.utils.startsWith("abc",/a/); }).not.toThrow();
expect(function() { $tw.utils.endsWith("abc",/c/); }).not.toThrow();
});
it("still match normal string inputs",function() {
expect($tw.utils.startsWith("abcdef","abc")).toBe(true);
expect($tw.utils.startsWith("abcdef","xyz")).toBe(false);
expect($tw.utils.endsWith("abcdef","def")).toBe(true);
expect($tw.utils.endsWith("abcdef","xyz")).toBe(false);
});
});
describe("$tw.utils.stringifyNumber",function() {
it("coerces null/undefined via string-concat rather than throwing",function() {
expect($tw.utils.stringifyNumber(null)).toBe("null");
expect($tw.utils.stringifyNumber(undefined)).toBe("undefined");
});
it("still returns a number's string form",function() {
expect($tw.utils.stringifyNumber(42)).toBe("42");
expect($tw.utils.stringifyNumber(-3.14)).toBe("-3.14");
expect($tw.utils.stringifyNumber(0)).toBe("0");
});
});
describe("$tw.utils.domContains",function() {
// Stub nodes expose both .contains() and .compareDocumentPosition() so
// both the old (compareDocumentPosition & 16 → number) and new
// (a !== b && a.contains(b) → boolean) forms can be observed.
function makeNode(children) {
children = children || [];
var self;
self = {
contains: function(other) {
if(other === self) { return true; }
return children.some(function(c) { return c === other || c.contains(other); });
},
compareDocumentPosition: function(other) {
if(other === self) { return 0; }
return self.contains(other) ? 16 : 0;
}
};
return self;
}
it("returns strictly boolean true/false, not a bit-mask number",function() {
var child = makeNode();
var parent = makeNode([child]);
var unrelated = makeNode();
expect($tw.utils.domContains(parent,child)).toBe(true);
expect($tw.utils.domContains(parent,unrelated)).toBe(false);
});
it("returns false for domContains(x, x)",function() {
var a = makeNode();
expect($tw.utils.domContains(a,a)).toBe(false);
});
});
describe("$tw.utils.hasClass",function() {
it("returns false for null/undefined element without throwing",function() {
expect(function() { $tw.utils.hasClass(null,"foo"); }).not.toThrow();
expect($tw.utils.hasClass(null,"foo")).toBe(false);
expect(function() { $tw.utils.hasClass(undefined,"foo"); }).not.toThrow();
expect($tw.utils.hasClass(undefined,"foo")).toBe(false);
});
it("returns strictly false (not undefined) for elements without classList",function() {
expect($tw.utils.hasClass({},"foo")).toBe(false);
});
it("delegates to classList.contains for real elements",function() {
var el = { classList: { contains: function(c) { return c === "a" || c === "b"; } } };
expect($tw.utils.hasClass(el,"a")).toBe(true);
expect($tw.utils.hasClass(el,"b")).toBe(true);
expect($tw.utils.hasClass(el,"c")).toBe(false);
});
});
// getLocationPath reads window.location: specs pend in Node (no `window`
// in the TW5 sandbox) and use history.replaceState in the browser —
// assigning to window.location would trigger a navigation and reload.
describe("$tw.utils.getLocationPath",function() {
var originalUrl;
beforeEach(function() {
if(!$tw.browser) { return; }
originalUrl = window.location.href;
});
afterEach(function() {
if(!$tw.browser) { return; }
history.replaceState(null,"",originalUrl);
});
it("preserves the query string in the returned path",function() {
if(!$tw.browser) { pending("browser-only: requires window.location - run in browser"); return; }
history.replaceState(null,"","?lang=de&x=1#Intro");
var path = $tw.utils.getLocationPath();
expect(path).toContain("?lang=de&x=1");
expect(path).not.toContain("#Intro");
});
it("strips the hash fragment",function() {
if(!$tw.browser) { pending("browser-only: requires window.location - run in browser"); return; }
history.replaceState(null,"","#SomeTiddler");
// Sanity check: replaceState actually changed the hash.
expect(window.location.hash).toBe("#SomeTiddler");
var path = $tw.utils.getLocationPath();
expect(path).not.toContain("#");
// Rebuild expected href without the hash — works on http(s):// and file://.
var expected = window.location.href.split("#")[0];
expect(path).toBe(expected);
});
it("includes the query string when no hash is present",function() {
if(!$tw.browser) { pending("browser-only: requires window.location - run in browser"); return; }
history.replaceState(null,"","?x=1");
var path = $tw.utils.getLocationPath();
expect(path).toMatch(/\?x=1$/);
expect(path).not.toContain("#");
});
});
// Regression guard: classList.add/remove/toggle throw InvalidCharacterError on
// whitespace. Manual repro: tw5-com #SampleWizard, class="aaa bbb", Done, popup
// -> OK -> nested popup -> RSOD. Stub classList mirrors real DOM semantics
// (reject whitespace, de-dupe on add, no-op on remove of missing token).
describe("addClass/removeClass/toggleClass",function() {
function makeEl() {
var tokens = [];
function reject(t) { if(/\s/.test(t)) { throw new Error("InvalidCharacterError: '" + t + "'"); } }
return {
classList: {
add: function() {
for(var i = 0; i < arguments.length; i++) {
reject(arguments[i]);
if(tokens.indexOf(arguments[i]) === -1) { tokens.push(arguments[i]); }
}
},
remove: function() {
for(var i = 0; i < arguments.length; i++) {
reject(arguments[i]);
var idx = tokens.indexOf(arguments[i]);
if(idx !== -1) { tokens.splice(idx,1); }
}
},
toggle: function(cls,status) {
reject(cls);
var has = tokens.indexOf(cls) !== -1;
var want = status === undefined ? !has : status;
if(want && !has) { tokens.push(cls); }
if(!want && has) { tokens.splice(tokens.indexOf(cls),1); }
}
},
_tokens: tokens
};
}
it("splits on every ASCII-whitespace flavour (space, tab, newline, CR, mixed runs, leading/trailing)",function() {
var el = makeEl();
$tw.utils.addClass(el," a\tb\nc\r\nd \t e ");
expect(el._tokens).toEqual(["a","b","c","d","e"]);
});
it("splits on Unicode whitespace too (U+00A0 non-breaking space, a common paste-in hazard)",function() {
var el = makeEl();
$tw.utils.addClass(el,"a\u00A0b");
expect(el._tokens).toEqual(["a","b"]);
});
it("de-duplicates tokens within one call and across calls",function() {
var el = makeEl();
$tw.utils.addClass(el,"x x y");
$tw.utils.addClass(el,"y z");
expect(el._tokens).toEqual(["x","y","z"]);
});
it("remove is a no-op for missing tokens and tolerates mixed-presence input",function() {
var el = makeEl();
$tw.utils.addClass(el,"a b");
$tw.utils.removeClass(el,"b c d");
expect(el._tokens).toEqual(["a"]);
});
it("toggle with no status flips each token independently",function() {
var el = makeEl();
$tw.utils.addClass(el,"a");
$tw.utils.toggleClass(el,"a b");
expect(el._tokens).toEqual(["b"]);
});
it("toggle with status=true/false forces state regardless of current",function() {
var el = makeEl();
$tw.utils.addClass(el,"a");
$tw.utils.toggleClass(el,"a b",true);
expect(el._tokens).toEqual(["a","b"]);
$tw.utils.toggleClass(el,"a b",false);
expect(el._tokens).toEqual([]);
});
it("is a silent no-op for whitespace-only / empty / non-string / null / undefined className",function() {
var el = makeEl();
var inputs = ["", " \t\n ", null, undefined, 42, {}, ["a"]];
inputs.forEach(function(v) {
expect(function() { $tw.utils.addClass(el,v); }).not.toThrow();
expect(function() { $tw.utils.removeClass(el,v); }).not.toThrow();
expect(function() { $tw.utils.toggleClass(el,v); }).not.toThrow();
});
expect(el._tokens).toEqual([]);
});
it("is a silent no-op when element has no classList (SVG in old browsers, detached nodes, stubs)",function() {
var el = {};
expect(function() { $tw.utils.addClass(el,"a b"); }).not.toThrow();
expect(function() { $tw.utils.removeClass(el,"a b"); }).not.toThrow();
expect(function() { $tw.utils.toggleClass(el,"a b",true); }).not.toThrow();
});
});
});
@@ -0,0 +1,138 @@
/*\
title: $:/tests/test-edit-widgets/helpers
type: application/javascript
module-type: library
Shared test helpers for the edit-text / edit-widget test suite. Every
helper here is usable in fakedom — nothing reaches for real DOM layout,
iframe documents, or synthetic events.
Import with:
var helpers = require("$:/tests/test-edit-widgets/helpers");
\*/
"use strict";
var widget = require("$:/core/modules/widgets/widget.js");
/*
makeWiki: build a fresh in-memory test wiki. Pass an array of tiddler
specs to seed. Use `seedTestTiddler: true` in the options to also add a
convenience "TestTiddler" (empty text) that many tests default to.
*/
exports.makeWiki = function(tiddlers,options) {
options = options || {};
var wiki = $tw.test.wiki();
if(options.seedTestTiddler) {
wiki.addTiddlers([{title: "TestTiddler", text: ""}]);
}
if(tiddlers) {
wiki.addTiddlers(tiddlers);
}
return wiki;
};
/*
parseAndRender: parse a wikitext snippet, build the widget tree against
the fakedom, and render into a detached wrapper <div>. Returns both the
top-level widgetNode (for refresh/teardown) and the wrapper (for DOM
inspection).
*/
exports.parseAndRender = function(widgetText,wiki) {
var parser = wiki.parseText("text/vnd.tiddlywiki",widgetText);
var widgetNode = new widget.widget(
{type: "widget", children: parser.tree},
{wiki: wiki, document: $tw.fakeDocument}
);
$tw.fakeDocument.setSequenceNumber(0);
var wrapper = $tw.fakeDocument.createElement("div");
widgetNode.render(wrapper,null);
return {widgetNode: widgetNode, wrapper: wrapper};
};
/*
findEditTextWidget: walk a widget tree and return the first
EditTextWidget instance (identified by having both an `engine` and an
`editTag` property). Needed because parseAndRender returns the
enclosing "widget" root, not the <$edit-text> itself.
*/
exports.findEditTextWidget = function findEditTextWidget(node) {
if(node.engine && node.editTag !== undefined) {
return node;
}
if(node.children) {
for(var i = 0; i < node.children.length; i++) {
var found = findEditTextWidget(node.children[i]);
if(found) return found;
}
}
return null;
};
/*
renderFromWikitext: parse+render a wikitext snippet and return a bundle
{ widget, root, wrapper, wiki } exposing every layer. Used by the
attribute-propagation and refresh tests which need access to the wiki
and wrapper after render.
*/
exports.renderFromWikitext = function(widgetText,wiki) {
wiki = wiki || exports.makeWiki([{title: "TestTiddler", text: "hello"}]);
var result = exports.parseAndRender(widgetText,wiki);
return {
widget: exports.findEditTextWidget(result.widgetNode),
root: result.widgetNode,
wrapper: result.wrapper,
wiki: wiki
};
};
/*
renderFromAttrs: build an <$edit-text> tag from an attrs object, render
it, and return just the EditTextWidget instance (not a full bundle).
Used by tests that only need the widget and its DOM node.
*/
exports.renderFromAttrs = function(attrs,wiki,tiddlerTitle) {
wiki = wiki || exports.makeWiki(null,{seedTestTiddler: true});
var attrStr = Object.keys(attrs).map(function(k) {
return k + "=\"" + attrs[k] + "\"";
}).join(" ");
var text = "<$edit-text tiddler=\"" + (tiddlerTitle || "TestTiddler") + "\" " + attrStr + "/>";
var rendered = exports.parseAndRender(text,wiki);
return exports.findEditTextWidget(rendered.widgetNode);
};
/*
refresh: drive a widget-tree refresh cycle for a given list of changed
tiddler titles, mimicking what the core does after wiki.addTiddler.
*/
exports.refresh = function(rootWidget,wrapper,changedTitles) {
var changed = {};
(changedTitles || []).forEach(function(t) { changed[t] = true; });
rootWidget.refresh(changed,wrapper,null);
};
/*
editorValue: read the editor's current text in a way that works with
fakedom. SimpleEngine seeds a textarea by appending a text-node child,
while inputs use the `value` attribute. Later updates go through
updateDomNodeText which sets `.value` on both.
*/
exports.editorValue = function(w) {
var dn = w.engine.domNode;
// If .value has been set explicitly, prefer it (covers refresh updates)
if(dn.attributes && dn.attributes.value !== undefined) {
return dn.value;
}
// Otherwise for textareas read the concatenated child text nodes
if(dn.tag === "textarea" && dn.children && dn.children.length) {
return dn.children.map(function(n) { return n.textContent || ""; }).join("");
}
return dn.value;
};
// Note: jasmine-specific helpers (spies) belong in the test-spec files
// themselves. TW library modules run in a sandbox that does not expose
// `jasmine`, so anything that calls jasmine.createSpy must live in a
// file tagged with $:/tags/test-spec.
@@ -0,0 +1,353 @@
/*\
title: test-edit-text-widget-attributes.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Covers edit-text widget behaviour that is observable in fakedom: value
resolution, tag/type selection, DOM attribute propagation, save-back
and refresh paths.
Each spec has a "manual:" comment with a by-hand recipe so a reviewer
can sanity-check the test against a live wiki.
NOT covered (need a real browser — Playwright territory):
* `focus`, `focusSelectFromStart`/`End`, `focusPopup`, `cancelPopups` — real DOM focus + selection APIs
* `inputActions`, `fileDrop` — synthetic events not dispatched by fakedom
* Pixel measurement / growth of auto-height textareas — `$tw.utils.resizeTextAreaToFit` needs real layout
* FramedEngine — requires a real iframe document
\*/
"use strict";
describe("Edit-text widget (attribute propagation and value handling)", function() {
// Shared helpers live in $:/tests/test-edit-widgets/helpers. See that
// file for what each helper prepares and where it is used across the suite.
var helpers = require("$:/tests/test-edit-widgets/helpers");
// Local aliases — `render`, `editorValue` etc. read better inline
// than `helpers.renderFromWikitext`. Each test supplies its own
// tiddlers, so makeWiki is NOT seeded with TestTiddler here.
var makeWiki = helpers.makeWiki;
var render = helpers.renderFromWikitext;
var refresh = helpers.refresh;
var editorValue = helpers.editorValue;
var findEditTextWidget = helpers.findEditTextWidget;
// spyOnSetText stays file-local because it depends on `jasmine`,
// which is only in scope for $:/tags/test-spec modules — NOT for
// library modules loaded via require().
function spyOnSetText(w) {
var spy = jasmine.createSpy("setText");
w.engine.setText = spy;
return spy;
}
// ---------------------------------------------------------------
// Value and type resolution (getEditInfo)
// ---------------------------------------------------------------
describe("value and type resolution", function() {
// manual: create tiddler "TestTiddler" with text "hello", then in any
// tiddler render <$edit-text tiddler='TestTiddler'/> — the
// textarea should show "hello".
it("reads the text field of an existing tiddler", function() {
var w = render("<$edit-text tiddler=\"TestTiddler\"/>");
expect(editorValue(w.widget)).toBe("hello");
});
// manual: give a tiddler "T" a caption="Hi" field, then render
// <$edit-text tiddler="T" field="caption"/> — input should show "Hi".
it("reads an arbitrary named field", function() {
var wiki = makeWiki([{title: "T", text: "body", caption: "Hi"}]);
var w = render("<$edit-text tiddler=\"T\" field=\"caption\"/>",wiki);
expect(editorValue(w.widget)).toBe("Hi");
});
// manual: create tiddler "T" without a caption field, then render
// <$edit-text tiddler="T" field="caption" default="fallback"/> —
// input should show "fallback".
it("falls back to the default attribute when the field is missing", function() {
var wiki = makeWiki([{title: "T", text: "body"}]);
var w = render("<$edit-text tiddler=\"T\" field=\"caption\" default=\"fallback\"/>",wiki);
expect(editorValue(w.widget)).toBe("fallback");
});
// manual: create tiddler "T" without a caption field, render
// <$edit-text tiddler="T" field="caption"/> — input should be empty.
it("falls back to an empty string when neither field nor default is present", function() {
var wiki = makeWiki([{title: "T", text: "body"}]);
var w = render("<$edit-text tiddler=\"T\" field=\"caption\"/>",wiki);
expect(editorValue(w.widget)).toBe("");
});
// manual: ensure NO tiddler named "Missing" exists, then render
// <$edit-text tiddler="Missing" field="title"/> — input should show
// "Missing" (the title field of a not-yet-existing tiddler defaults
// to the tiddler title).
it("uses the tiddler title as the default for the title field on missing tiddlers", function() {
var wiki = makeWiki();
var w = render("<$edit-text tiddler=\"Missing\" field=\"title\"/>",wiki);
expect(editorValue(w.widget)).toBe("Missing");
});
// manual: ensure NO tiddler named "Missing" exists, then render
// <$edit-text tiddler="Missing" default="seed"/> — textarea should
// show "seed" until you type into it.
it("uses default= on a missing tiddler", function() {
var wiki = makeWiki();
var w = render("<$edit-text tiddler=\"Missing\" default=\"seed\"/>",wiki);
expect(editorValue(w.widget)).toBe("seed");
});
// manual: create a data tiddler "Data" with type
// "application/x-tiddler-dictionary" and text "one: 1\ntwo: 2",
// then render <$edit-text tiddler="Data" index="two"/> — input
// should show "2".
it("reads a value from a data tiddler index", function() {
var wiki = makeWiki([
{title: "Data", type: "application/x-tiddler-dictionary", text: "one: 1\ntwo: 2"}
]);
var w = render("<$edit-text tiddler=\"Data\" index=\"two\"/>",wiki);
expect(editorValue(w.widget)).toBe("2");
});
// manual: with the same "Data" dictionary but no "missing" key,
// render <$edit-text tiddler="Data" index="missing" default="none"/> —
// input should show "none".
it("uses default= for a missing index", function() {
var wiki = makeWiki([
{title: "Data", type: "application/x-tiddler-dictionary", text: "one: 1"}
]);
var w = render("<$edit-text tiddler=\"Data\" index=\"missing\" default=\"none\"/>",wiki);
expect(editorValue(w.widget)).toBe("none");
});
});
// ---------------------------------------------------------------
// Tag and type selection
// ---------------------------------------------------------------
describe("tag and type selection", function() {
// manual: render <$edit-text tiddler="TestTiddler"/> and inspect the
// element — it should be a <textarea>.
it("defaults to a textarea for the text field", function() {
var w = render("<$edit-text tiddler=\"TestTiddler\"/>");
expect(w.widget.engine.domNode.tag).toBe("textarea");
});
// manual: render <$edit-text tiddler="TestTiddler" field="caption"/>
// and inspect the element — it should be a single-line <input>.
it("defaults to an input for non-text fields", function() {
var w = render("<$edit-text tiddler=\"TestTiddler\" field=\"caption\"/>");
expect(w.widget.engine.domNode.tag).toBe("input");
});
// manual: render <$edit-text tiddler="TestTiddler" tag="input"/> —
// even though the default for the text field is textarea, this
// should render as a single-line <input>.
it("tag=input forces an input even for the text field", function() {
var w = render("<$edit-text tiddler=\"TestTiddler\" tag=\"input\"/>");
expect(w.widget.engine.domNode.tag).toBe("input");
});
// manual: render <$edit-text tiddler="T" tag="input" type="password"/>
// — the element should be a password input (characters masked as dots).
it("type= sets the input type attribute", function() {
var w = render("<$edit-text tiddler=\"TestTiddler\" tag=\"input\" type=\"password\"/>");
expect(w.widget.engine.domNode.getAttribute("type")).toBe("password");
});
// manual: render <$edit-text tiddler="T" type="password"/> (no tag
// override) — the element should be a plain textarea; type is
// silently ignored because HTML textareas have no type attribute.
it("type= is ignored for textareas (no type attribute on textarea)", function() {
var w = render("<$edit-text tiddler=\"TestTiddler\" type=\"password\"/>");
expect(w.widget.engine.domNode.getAttribute("type")).toBeUndefined();
});
// manual: render <$edit-text tiddler="T" tag="script"/> — must NOT
// inject a <script> element; tag should fall back to <input>. This
// is a defence-in-depth check against wiki-authored HTML injection.
it("unsafe tag names collapse to input", function() {
var w = render("<$edit-text tiddler=\"TestTiddler\" tag=\"script\"/>");
expect(w.widget.engine.domNode.tag).toBe("input");
});
});
// ---------------------------------------------------------------
// DOM attribute propagation
// ---------------------------------------------------------------
describe("DOM attribute propagation", function() {
// manual: render <$edit-text tiddler="TestTiddler" tag="input"
// class="my-class" placeholder="Type here" size="40" tabindex="3"
// autocomplete="email" disabled="yes"/> — inspect the element in
// devtools; every attribute set on the widget should appear on
// the DOM element with the expected value. disabled="no" would
// omit the attribute entirely.
//
// INFO: View as "raw HTML" in preview panel
//
// SimpleEngine has one independent `if(this.widget.editXxx)
// setAttribute(...)` block per attribute. This combined test
// exercises every such block in one shot; if any copy breaks,
// this spec fails and the jasmine expectation report identifies
// the offending attribute.
it("copies class, placeholder, size, tabindex, autocomplete and disabled onto the DOM element", function() {
var w = render(
"<$edit-text tiddler=\"TestTiddler\" tag=\"input\" " +
"class=\"my-class\" placeholder=\"Type here\" size=\"40\" " +
"tabindex=\"3\" autocomplete=\"email\" disabled=\"yes\"/>"
);
var dn = w.widget.engine.domNode;
expect(dn.className).toBe("my-class");
expect(dn.getAttribute("placeholder")).toBe("Type here");
expect(dn.getAttribute("size")).toBe("40");
expect(dn.getAttribute("tabindex")).toBe("3");
expect(dn.getAttribute("autocomplete")).toBe("email");
expect(dn.getAttribute("disabled")).toBe("true");
});
});
// ---------------------------------------------------------------
// Save-back via saveChanges
// ---------------------------------------------------------------
describe("saving changes back to the store", function() {
// manual: with tiddler "T" text="old", render <$edit-text tiddler="T"/>
// and type "new text"; open T from the sidebar — its text field
// should update live.
it("writes a new value to the configured text field", function() {
var wiki = makeWiki([{title: "T", text: "old"}]);
var w = render("<$edit-text tiddler=\"T\"/>",wiki);
w.widget.saveChanges("new text");
expect(wiki.getTiddler("T").fields.text).toBe("new text");
});
// manual: with tiddler "T" caption="old", render
// <$edit-text tiddler="T" field="caption"/> and type a new caption;
// {{T!!caption}} elsewhere should update live.
it("writes to a non-text field", function() {
var wiki = makeWiki([{title: "T", text: "body", caption: "old"}]);
var w = render("<$edit-text tiddler=\"T\" field=\"caption\"/>",wiki);
w.widget.saveChanges("new caption");
expect(wiki.getTiddler("T").fields.caption).toBe("new caption");
});
// manual: ensure no tiddler "NewOne" exists, render
// <$edit-text tiddler="NewOne" default=""/> and type "created" —
// a new tiddler "NewOne" should appear in the sidebar with that
// text.
it("creates the tiddler if it does not exist", function() {
var wiki = makeWiki();
var w = render("<$edit-text tiddler=\"NewOne\" default=\"\"/>",wiki);
w.widget.saveChanges("created");
expect(wiki.tiddlerExists("NewOne")).toBe(true);
expect(wiki.getTiddler("NewOne").fields.text).toBe("created");
});
// manual: with dictionary tiddler "Data" containing one:1, two:2,
// render <$edit-text tiddler="Data" index="one"/> and change the
// value to "uno"; view Data as text — it should now read
// "one: uno\ntwo: 2".
it("writes to a data tiddler index", function() {
var wiki = makeWiki([
{title: "Data", type: "application/x-tiddler-dictionary", text: "one: 1\ntwo: 2"}
]);
var w = render("<$edit-text tiddler=\"Data\" index=\"one\"/>",wiki);
w.widget.saveChanges("uno");
expect(wiki.extractTiddlerDataItem("Data","one")).toBe("uno");
// Other index entries preserved
expect(wiki.extractTiddlerDataItem("Data","two")).toBe("2");
});
// manual: Select the text. Copy / Paste -> modified does not change
it("skips the store update when the value is unchanged", function() {
var wiki = makeWiki([{title: "T", text: "same"}]);
var before = wiki.getTiddler("T").fields.modified;
var w = render("<$edit-text tiddler=\"T\"/>",wiki);
w.widget.saveChanges("same");
expect(wiki.getTiddler("T").fields.modified).toBe(before);
});
});
// ---------------------------------------------------------------
// Refresh behaviour
// ---------------------------------------------------------------
describe("refresh behaviour", function() {
// manual: render <$edit-text tiddler="T"/>, in New Tiddler and New Tiddler 1
// The textarea you are NOT focused on
// should live-update to reflect the new text field.
it("propagates an external change of the edited tiddler through engine.setText", function() {
var wiki = makeWiki([{title: "T", text: "first"}]);
var w = render("<$edit-text tiddler=\"T\"/>",wiki);
expect(editorValue(w.widget)).toBe("first");
var setTextSpy = spyOnSetText(w.widget);
wiki.addTiddler({title: "T", text: "second"});
refresh(w.root,w.wrapper,["T"]);
expect(setTextSpy).toHaveBeenCalled();
expect(setTextSpy.calls.mostRecent().args[0]).toBe("second");
});
// manual:
// 1. Create tiddlers T (text "initial") and Trigger (text "x").
// 2. Render <$edit-text tiddler="T" refreshTitle="Trigger"/>.
// 3. Click into the textarea (it now has focus) and type " typed".
// 4. Open F12, in console:
// $tw.wiki.addTiddler({title:"T",text:"external"})
// Focus stays on textarea. Store changes, but setText skips
// the DOM write because the node is focused — DOM diverges
// from store (textarea still shows "initial typed").
// 5. In console:
// $tw.wiki.addTiddler({title:"Trigger",text:"bump"})
// Textarea should VISIBLY flip to "external" while still
// focused. updateDomNodeText has no focus check.
// Without refreshTitle, step 5 does nothing — DOM stays diverged.
it("refreshTitle triggers updateDomNodeText even when a different tiddler changes", function() {
var wiki = makeWiki([
{title: "T", text: "first"},
{title: "Trigger", text: "x"}
]);
var w = render("<$edit-text tiddler=\"T\" refreshTitle=\"Trigger\"/>",wiki);
// refreshTitle goes through engine.updateDomNodeText, not setText
var updateSpy = jasmine.createSpy("updateDomNodeText");
w.widget.engine.updateDomNodeText = updateSpy;
wiki.addTiddler({title: "T", text: "updated"});
refresh(w.root,w.wrapper,["Trigger"]);
expect(updateSpy).toHaveBeenCalledWith("updated");
});
// manual: render <$edit-text tiddler="T" class={{ClassSrc}}/> and
// change ClassSrc's text. Inspect the textarea element in devtools
// — the class attribute should flip to the new value. This covers
// a full re-render (refreshSelf), not just an in-place text update.
it("changes to attributes that require a full rerender trigger refreshSelf", function() {
// When an attribute like `class` changes via a transcluded variable,
// the widget should rerender itself. We simulate that by wrapping the
// edit-text inside a transclusion where the class is indirect.
var wiki = makeWiki([
{title: "T", text: "body"},
{title: "ClassSrc", text: "first-class"}
]);
var w = render(
"<$edit-text tiddler=\"T\" class={{ClassSrc}}/>",
wiki
);
expect(w.widget.engine.domNode.className).toBe("first-class");
wiki.addTiddler({title: "ClassSrc", text: "second-class"});
refresh(w.root,w.wrapper,["ClassSrc"]);
// Re-locate the widget because a full refresh creates a new instance
var fresh = findEditTextWidget(w.root);
expect(fresh.engine.domNode.className).toBe("second-class");
});
});
});
@@ -0,0 +1,198 @@
/*\
title: test-edit-text-widget.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Covers the `rows` / `autoHeight` / `minHeight` interaction in the
edit-text widget. Regression guard for issue #9451 / PR #9454 follow-up
fix (rows=1 autoHeight=yes must still grow).
Assertions target the routing inside SimpleEngine.fixHeight (which
branch runs, called with which minHeight) — pixel measurement needs a
real browser.
\*/
"use strict";
describe("Edit-text widget", function() {
// Shared helpers live in $:/tests/test-edit-widgets/helpers. See that
// file for what each helper prepares and where it is used across the suite.
var helpers = require("$:/tests/test-edit-widgets/helpers");
// Local wrapper: every test in THIS file seeds an empty "TestTiddler"
// as its default binding target, so we inject that by default.
function makeWiki(extraTiddlers) {
return helpers.makeWiki(extraTiddlers,{seedTestTiddler: true});
}
// Local alias — `renderEditText` reads more naturally than
// `helpers.renderFromAttrs` for the routing tests.
function renderEditText(attrs,wiki,tiddlerTitle) {
return helpers.renderFromAttrs(attrs,wiki,tiddlerTitle);
}
// ---------------------------------------------------------------
// Attribute parsing
// ---------------------------------------------------------------
describe("attribute parsing", function() {
// manual: on a bare <$edit-text tiddler="T"/>, open devtools and
// inspect the widget instance — the element should be a textarea,
// there should be no rows attribute, and auto-height should be on.
// Manual Preview: Also use the widget-tree
it("defaults: tag=textarea, autoHeight=yes, no rows, minHeight=100px", function() {
var et = renderEditText({});
expect(et.editTag).toBe("textarea");
expect(et.editAutoHeight).toBe(true);
expect(et.editRows).toBeUndefined();
expect(et.editMinHeight).toBe("100px");
});
// manual: render <$edit-text tiddler="T" rows=5 autoHeight="no" minHeight="1em"/>
// — the widget's internal state should reflect all three explicit
// values, overriding the defaults verified in the previous test.
// Manual Preview: Also use the widget-tree
it("explicit attributes override defaults (rows, autoHeight, minHeight)", function() {
var et = renderEditText({rows: "5", autoHeight: "no", minHeight: "1em"});
expect(et.editRows).toBe("5");
expect(et.editAutoHeight).toBe(false);
expect(et.editMinHeight).toBe("1em");
});
// manual: render <$edit-text tiddler="T" field="caption" tag="textarea"/>
// — the element should be a multi-line textarea even though the
// field is not `text`.
it("tag=textarea override applies to non-text fields", function() {
var et = renderEditText({field: "caption", tag: "textarea"});
expect(et.editTag).toBe("textarea");
});
// manual: set $:/config/TextEditor/EditorHeight/Mode = "fixed"
// (via control panel → settings → editor toolbar, or edit the
// tiddler directly). <$edit-text tiddler="T"/> should now behave
// as fixed-height (scrollbar on overflow) by default.
it("$:/config/TextEditor/EditorHeight/Mode=fixed flips default autoHeight to false", function() {
var wiki = makeWiki([
{title: "$:/config/TextEditor/EditorHeight/Mode", text: "fixed"}
]);
var et = renderEditText({},wiki);
expect(et.editAutoHeight).toBe(false);
});
// manual: with the same Mode=fixed config as above, an explicit
// <$edit-text tiddler="T" autoHeight="yes"/> should still auto-grow
// — the per-widget attribute wins over the global config.
it("explicit autoHeight=yes overrides Mode=fixed config", function() {
var wiki = makeWiki([
{title: "$:/config/TextEditor/EditorHeight/Mode", text: "fixed"}
]);
var et = renderEditText({autoHeight: "yes"},wiki);
expect(et.editAutoHeight).toBe(true);
});
});
// ---------------------------------------------------------------
// DOM node construction (SimpleEngine)
// ---------------------------------------------------------------
describe("DOM node construction (non-toolbar SimpleEngine)", function() {
// manual: render <$edit-text tiddler="T" rows=1/> and inspect the
// element — it should carry rows="1". Proves SimpleEngine copies
// widget.editRows onto the DOM element.
it("sets the rows attribute on the DOM node when rows is specified", function() {
var et = renderEditText({rows: "1"});
expect(et.engine.domNode.getAttribute("rows")).toBe("1");
});
});
// ---------------------------------------------------------------
// fixHeight routing — the core of the fix
// ---------------------------------------------------------------
describe("fixHeight routing", function() {
var originalResize, resizeSpy;
beforeEach(function() {
originalResize = $tw.utils.resizeTextAreaToFit;
resizeSpy = jasmine.createSpy("resizeTextAreaToFit").and.returnValue(0);
$tw.utils.resizeTextAreaToFit = resizeSpy;
});
afterEach(function() {
$tw.utils.resizeTextAreaToFit = originalResize;
});
// Make the SimpleEngine.fixHeight think we are in a real DOM so it
// actually reaches the $tw.utils.resizeTextAreaToFit call. Without
// this the engine short-circuits because fakedom nodes carry
// isTiddlyWikiFakeDom === true.
function callFixHeight(et) {
et.engine.domNode.isTiddlyWikiFakeDom = false;
et.engine.fixHeight();
}
// manual: render <$edit-text tiddler="T" rows=1 autoHeight=yes minHeight=1em/>
// (the TiddlyTools idiom). In 5.4.0 pre-fix, the textarea stayed
// a single row forever; after the fix, it starts at 1 row and
// grows as you type newlines. This covers BRANCH A (autoHeight=yes)
// of SimpleEngine.fixHeight: resize is called with minHeight even
// when rows is set. Strict superset of the no-rows baseline.
it("rows=1 autoHeight=yes minHeight=1em still calls resize (regression test for the PR #9454 follow-up fix)", function() {
var et = renderEditText({rows: "1", autoHeight: "yes", minHeight: "1em"});
callFixHeight(et);
expect(resizeSpy).toHaveBeenCalled();
expect(resizeSpy.calls.mostRecent().args[1]).toBe("1em");
});
// manual: render <$edit-text tiddler="T" rows=5 autoHeight=no/> —
// the textarea must stay locked at exactly 5 rows tall, even if
// you paste in 50 lines (vertical scrollbar appears inside).
it("rows=5 autoHeight=no does NOT call resize and does NOT apply the fixed-height fallback", function() {
var et = renderEditText({rows: "5", autoHeight: "no"});
callFixHeight(et);
expect(resizeSpy).not.toHaveBeenCalled();
// rows attribute governs height — CSS height must remain unset
expect(et.engine.domNode.style.height).toBe("");
});
// manual: set $:/config/TextEditor/EditorHeight/Height = "250px",
// then render <$edit-text tiddler="T" autoHeight="no"/> — the
// textarea should be exactly 250px tall regardless of content.
it("autoHeight=no with no rows applies the fixed-height fallback from config", function() {
var wiki = makeWiki([
{title: "$:/config/TextEditor/EditorHeight/Height", text: "250px"}
]);
var et = renderEditText({autoHeight: "no"},wiki);
callFixHeight(et);
expect(resizeSpy).not.toHaveBeenCalled();
expect(et.engine.domNode.style.height).toBe("250px");
});
// manual: set $:/config/TextEditor/EditorHeight/Height = "5px",
// render <$edit-text tiddler="T" autoHeight="no"/> — the textarea
// should be 20px tall (clamped), not a sliver, so it stays usable.
it("autoHeight=no fallback height is clamped to at least 20px", function() {
var wiki = makeWiki([
{title: "$:/config/TextEditor/EditorHeight/Height", text: "5px"}
]);
var et = renderEditText({autoHeight: "no"},wiki);
callFixHeight(et);
expect(et.engine.domNode.style.height).toBe("20px");
});
// manual: render <$edit-text tiddler="T" field="caption" autoHeight="no"/>
// — the element is a single-line <input> and the CSS height should
// remain unset (no height fallback applies to inputs).
it("tag=input: neither resize nor fixed-height fallback runs", function() {
var et = renderEditText({field: "caption", autoHeight: "no"});
callFixHeight(et);
expect(resizeSpy).not.toHaveBeenCalled();
expect(et.engine.domNode.style.height).toBe("");
});
});
});
@@ -18,4 +18,65 @@ describe("fakedom tests", function() {
expect($tw.fakeDocument.createTextNode("text").nodeType).toBe(3);
expect($tw.fakeDocument.createTextNode("text").TEXT_NODE).toBe(3);
});
// Per DOM spec, tagName returns the HTML-uppercased qualified name for HTML
// elements. Other namespaces preserve case.
// https://dom.spec.whatwg.org/#dom-element-tagname
var HTML_NS = "http://www.w3.org/1999/xhtml";
var SVG_NS = "http://www.w3.org/2000/svg";
it("tagName uppercases for HTML namespace", function() {
// Default namespace is HTML
expect($tw.fakeDocument.createElement("div").tagName).toBe("DIV");
// The exact predicate the select widget relies on (#9839)
expect($tw.fakeDocument.createElement("optgroup").tagName === "OPTGROUP").toBe(true);
// Already-uppercase input stays uppercase
expect($tw.fakeDocument.createElement("OPTGROUP").tagName).toBe("OPTGROUP");
// Mixed-case input is normalised
expect($tw.fakeDocument.createElement("Div").tagName).toBe("DIV");
// Hyphenated custom-element names uppercase whole tag, hyphens survive
expect($tw.fakeDocument.createElement("my-button").tagName).toBe("MY-BUTTON");
// Empty tag returns empty string
expect($tw.fakeDocument.createElement("").tagName).toBe("");
// Explicit HTML namespace via createElementNS uppercases the same way
expect($tw.fakeDocument.createElementNS(HTML_NS,"Div").tagName).toBe("DIV");
});
it("tagName preserves case for non-HTML namespaces", function() {
// SVG: lowercase preserved
expect($tw.fakeDocument.createElementNS(SVG_NS,"circle").tagName).toBe("circle");
// SVG: camelCase preserved (linearGradient is the canonical example)
expect($tw.fakeDocument.createElementNS(SVG_NS,"linearGradient").tagName).toBe("linearGradient");
// SVG: already-uppercase input is also preserved (NOT lowercased)
expect($tw.fakeDocument.createElementNS(SVG_NS,"DIV").tagName).toBe("DIV");
// Empty namespace string is "no namespace", not HTML. Case preserved.
expect($tw.fakeDocument.createElementNS("","div").tagName).toBe("div");
});
it("tagName reflects current state without mutating it", function() {
// Reading tagName must not overwrite the internal `tag` field
var el = $tw.fakeDocument.createElement("div");
expect(el.tagName).toBe("DIV");
expect(el.tag).toBe("div");
// Idempotent: two reads return identical values
var first = el.tagName, second = el.tagName;
expect(first).toBe(second);
// Dynamic namespace change is reflected. The getter must read current
// state, not a value cached at construction time.
var dynamic = $tw.fakeDocument.createElement("foo");
expect(dynamic.tagName).toBe("FOO");
dynamic.namespaceURI = SVG_NS;
expect(dynamic.tagName).toBe("foo");
});
// Real CSSStyleDeclaration returns undefined for Symbol property keys.
// Without a guard, the TW_Style Proxy throws on Symbol access. This bites
// in practice when Jasmine pretty-prints fakedom elements on failure.
// See related TODO in test-select-widget.js
it("returns undefined for Symbol property access on element.style", function() {
var el = $tw.fakeDocument.createElement("div");
expect(function() { return el.style[Symbol.toPrimitive]; }).not.toThrow();
expect(el.style[Symbol.toPrimitive]).toBeUndefined();
expect(function() { el.style[Symbol.iterator] = "x"; }).not.toThrow();
});
});
@@ -0,0 +1,179 @@
/*\
title: test-filesystem-dynamic-store.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests for the filesystem syncadaptor dynamic store feature: save routing
driven by saveFilter, and chokidar-based watching of out-of-band edits.
\*/
"use strict";
if($tw.node) {
var fs = require("fs"),
path = require("path"),
os = require("os");
// Load the filesystem adaptor source as if it were a TW module, so that
// $tw is provided without having to include the plugin in the edition
// (which would pull in the server-side syncer and keep the test runner alive).
var adaptorPath = path.resolve($tw.boot.bootPath,"..","plugins","tiddlywiki","filesystem","filesystemadaptor.js"),
adaptorTitle = "$:/plugins/tiddlywiki/filesystem/filesystemadaptor.js";
if(!$tw.modules.titles[adaptorTitle]) {
$tw.modules.titles[adaptorTitle] = {
moduleType: "syncadaptor",
definition: fs.readFileSync(adaptorPath,"utf8")
};
$tw.wiki.addTiddler({
title: adaptorTitle,
type: "application/javascript",
"module-type": "syncadaptor",
text: ""
});
}
var FileSystemAdaptor = $tw.modules.execute(adaptorTitle).adaptorClass;
function makeTempDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(),prefix));
}
function removeDirRecursive(dir) {
if(fs.existsSync(dir)) {
fs.rmSync(dir,{recursive: true, force: true});
}
}
describe("filesystem dynamic store", function() {
var tmpRoot, wikiTiddlers, storeDir, origDynamicStores, origFiles, originalBootPath;
var adaptor, wiki;
beforeEach(function() {
tmpRoot = makeTempDir("tw-dyn-");
wikiTiddlers = path.join(tmpRoot,"tiddlers");
storeDir = path.join(tmpRoot,"content");
fs.mkdirSync(wikiTiddlers);
fs.mkdirSync(storeDir);
origDynamicStores = $tw.boot.dynamicStores;
origFiles = $tw.boot.files;
originalBootPath = $tw.boot.wikiTiddlersPath;
$tw.boot.dynamicStores = [{
id: storeDir,
directory: storeDir,
saveFilter: "[type[text/x-markdown]]",
watch: true,
debounce: 40,
filesRegExp: ".*\\.tid$",
searchSubdirectories: false,
isTiddlerFile: true,
fields: {}
}];
$tw.boot.files = Object.create(null);
$tw.boot.wikiTiddlersPath = wikiTiddlers;
wiki = new $tw.Wiki();
adaptor = new FileSystemAdaptor({wiki: wiki, boot: $tw.boot});
});
afterEach(function(done) {
adaptor.close().then(function() {
$tw.boot.dynamicStores = origDynamicStores;
$tw.boot.files = origFiles;
$tw.boot.wikiTiddlersPath = originalBootPath;
removeDirRecursive(tmpRoot);
done();
});
});
it("routes saves for matching tiddlers into the dynamic store directory", function(done) {
wiki.addTiddler(new $tw.Tiddler({title: "note1", type: "text/x-markdown", text: "hello"}));
var tiddler = wiki.getTiddler("note1");
adaptor.getTiddlerFileInfo(tiddler,function(err,fileInfo) {
expect(err).toBeFalsy();
expect(fileInfo.filepath.indexOf(storeDir)).toBe(0);
expect(fileInfo.dynamicStoreId).toBe(storeDir);
done();
});
});
it("routes saves for non-matching tiddlers into the default wiki tiddlers path", function(done) {
wiki.addTiddler(new $tw.Tiddler({title: "note2", type: "text/vnd.tiddlywiki", text: "plain"}));
var tiddler = wiki.getTiddler("note2");
adaptor.getTiddlerFileInfo(tiddler,function(err,fileInfo) {
expect(err).toBeFalsy();
expect(fileInfo.filepath.indexOf(wikiTiddlers)).toBe(0);
expect(fileInfo.dynamicStoreId).toBeFalsy();
done();
});
});
it("keeps saving a tiddler into the store it originally came from", function(done) {
// Simulate a tiddler that was loaded at boot from the dynamic store
$tw.boot.files["frozen"] = {
filepath: path.join(storeDir,"frozen.tid"),
type: "application/x-tiddler",
hasMetaFile: false,
isEditableFile: true,
dynamicStoreId: storeDir
};
// Its current type no longer matches the saveFilter — store id must still win
wiki.addTiddler(new $tw.Tiddler({title: "frozen", type: "text/vnd.tiddlywiki", text: "still here"}));
adaptor.getTiddlerFileInfo(wiki.getTiddler("frozen"),function(err,fileInfo) {
expect(err).toBeFalsy();
expect(fileInfo.filepath.indexOf(storeDir)).toBe(0);
expect(fileInfo.dynamicStoreId).toBe(storeDir);
done();
});
});
// Note: the chokidar watcher's only job is to call processFileEvent in
// response to fs events. We invoke processFileEvent directly here so the
// tests don't depend on real fs notifications being delivered (some CI
// sandboxes do not propagate inotify events to chokidar).
it("processes external additions, changes and deletions", function(done) {
var store = $tw.boot.dynamicStores[0];
var filepath = path.join(storeDir,"external.tid");
fs.writeFileSync(filepath,"title: external\ntype: text/x-markdown\n\nInitial\n");
adaptor.processFileEvent(store,filepath,"change");
adaptor.getUpdatedTiddlers({},function(err,updates) {
expect(err).toBeFalsy();
expect(updates.modifications).toContain("external");
adaptor.loadTiddler("external",function(err,fields) {
expect(err).toBeFalsy();
expect(fields).toBeTruthy();
expect(fields.title).toBe("external");
expect(fields.text).toContain("Initial");
// Edit
fs.writeFileSync(filepath,"title: external\ntype: text/x-markdown\n\nChanged\n");
adaptor.processFileEvent(store,filepath,"change");
adaptor.getUpdatedTiddlers({},function(err,updates) {
expect(updates.modifications).toContain("external");
// Delete
fs.unlinkSync(filepath);
adaptor.processFileEvent(store,filepath,"unlink");
adaptor.getUpdatedTiddlers({},function(err,updates) {
expect(updates.deletions).toContain("external");
done();
});
});
});
});
});
it("suppresses echoes when the file on disk matches the current wiki tiddler", function(done) {
var store = $tw.boot.dynamicStores[0];
wiki.addTiddler(new $tw.Tiddler({title: "echo", type: "text/x-markdown", text: "same\n"}));
var filepath = path.join(storeDir,"echo.tid");
fs.writeFileSync(filepath,"title: echo\ntype: text/x-markdown\n\nsame\n");
adaptor.processFileEvent(store,filepath,"change");
adaptor.getUpdatedTiddlers({},function(err,updates) {
expect(updates.modifications).not.toContain("echo");
done();
});
});
});
}
+1 -1
View File
@@ -1062,7 +1062,7 @@ describe("Filter tests", function() {
});
it("should handle the deserializers operator", function() {
var expectedDeserializers = ["application/javascript","application/json","application/x-tiddler","application/x-tiddler-html-div","application/x-tiddlers","text/css","text/html","text/plain"];
var expectedDeserializers = ["application/javascript","application/json","application/x-tiddler","application/x-tiddler-html-div","application/x-tiddlers","text/css","text/html","text/markdown","text/plain","text/x-markdown"];
if($tw.browser) {
expectedDeserializers.unshift("(DOM)");
}
@@ -0,0 +1,301 @@
/*\
title: test-markdown-frontmatter.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests for the markdown plugin's YAML frontmatter parser, deserializer,
and serializer.
\*/
/* eslint-env node, browser, jasmine */
/* eslint no-mixed-spaces-and-tabs: ["error", "smart-tabs"]*/
"use strict";
describe("markdown YAML frontmatter", function() {
var yaml = require("$:/plugins/tiddlywiki/markdown/yaml.js");
var deserializer = require("$:/plugins/tiddlywiki/markdown/frontmatter-deserializer.js");
var serializer = require("$:/plugins/tiddlywiki/markdown/frontmatter-serializer.js");
// --- YAML parser ---
describe("yaml.load scalars", function() {
it("parses null forms", function() {
expect(yaml.load("null")).toBe(null);
expect(yaml.load("~")).toBe(null);
expect(yaml.load("")).toBe(null);
});
it("parses booleans", function() {
expect(yaml.load("true")).toBe(true);
expect(yaml.load("True")).toBe(true);
expect(yaml.load("false")).toBe(false);
});
it("parses numbers", function() {
expect(yaml.load("42")).toBe(42);
expect(yaml.load("-7")).toBe(-7);
expect(yaml.load("3.14")).toBe(3.14);
expect(yaml.load("1e10")).toBe(1e10);
expect(yaml.load("0xFF")).toBe(255);
expect(yaml.load("0o17")).toBe(15);
});
it("parses special floats", function() {
expect(yaml.load(".inf")).toBe(Infinity);
expect(yaml.load("-.inf")).toBe(-Infinity);
});
it("parses quoted strings", function() {
expect(yaml.load('"hello world"')).toBe("hello world");
expect(yaml.load("'hello world'")).toBe("hello world");
expect(yaml.load('"line1\\nline2"')).toBe("line1\nline2");
});
it("parses plain strings", function() {
expect(yaml.load("hello")).toBe("hello");
});
it("rejects non-strings", function() {
expect(function() { yaml.load(123); }).toThrowError(yaml.YAMLException);
});
});
describe("yaml.load flow collections", function() {
it("parses flow sequences", function() {
expect(yaml.load("[a, b, c]")).toEqual(["a","b","c"]);
expect(yaml.load("[1, 2, 3]")).toEqual([1,2,3]);
expect(yaml.load('[1, "two", true, null]')).toEqual([1,"two",true,null]);
expect(yaml.load("[]")).toEqual([]);
expect(yaml.load('["multi word", simple]')).toEqual(["multi word","simple"]);
});
it("parses flow mappings", function() {
expect(yaml.load("{a: 1, b: 2}")).toEqual({a:1,b:2});
expect(yaml.load("{}")).toEqual({});
});
});
describe("yaml.load block collections", function() {
it("parses simple block mappings", function() {
expect(yaml.load("title: Hello\ntags: foo bar\nrating: 6")).toEqual({
title: "Hello",
tags: "foo bar",
rating: 6
});
});
it("parses block mapping with flow array value", function() {
expect(yaml.load("title: Test\ntags: [concept, synthesis, multi word tag]")).toEqual({
title: "Test",
tags: ["concept","synthesis","multi word tag"]
});
});
it("parses block mapping with quoted value", function() {
expect(yaml.load('title: "A: Subtitle"')).toEqual({title: "A: Subtitle"});
});
it("parses block mapping with null value", function() {
expect(yaml.load("title: Test\ndescription:")).toEqual({
title: "Test",
description: null
});
});
it("parses block sequences", function() {
expect(yaml.load("- alpha\n- beta\n- gamma")).toEqual(["alpha","beta","gamma"]);
expect(yaml.load("- 1\n- two\n- true")).toEqual([1,"two",true]);
});
it("parses nested block mappings", function() {
expect(yaml.load("outer:\n inner: value\n count: 3")).toEqual({
outer: {inner: "value", count: 3}
});
});
it("parses block mapping with block sequence value", function() {
expect(yaml.load("title: Test\ntags:\n - concept\n - synthesis")).toEqual({
title: "Test",
tags: ["concept","synthesis"]
});
});
it("ignores comments and blank lines", function() {
expect(yaml.load("# comment\ntitle: Test\n# more\nrating: 5")).toEqual({
title: "Test",
rating: 5
});
});
});
describe("yaml.dump", function() {
it("dumps simple mappings", function() {
expect(yaml.dump({title: "Hello", rating: 6}).trim()).toBe("title: Hello\nrating: 6");
});
it("dumps arrays", function() {
expect(yaml.dump({tags: ["a","b"]}).trim()).toBe("tags:\n - a\n - b");
});
it("dumps null and booleans", function() {
expect(yaml.dump({x: null}).trim()).toBe("x: null");
expect(yaml.dump({x: true, y: false}).trim()).toBe("x: true\ny: false");
});
it("dumps empty containers", function() {
expect(yaml.dump({}).trim()).toBe("{}");
expect(yaml.dump({x: []}).trim()).toBe("x: []");
});
it("quotes string values that look like numbers", function() {
expect(yaml.dump({rating: "9"}).trim()).toBe('rating: "9"');
});
});
// --- Deserializer ---
describe("frontmatter deserializer", function() {
var ds = deserializer["text/x-markdown"];
it("extracts simple frontmatter into fields", function() {
var result = ds("---\ntitle: Foo\ntags: [a, b]\n---\n\nBody text.",{});
expect(result.length).toBe(1);
expect(result[0].title).toBe("Foo");
expect(result[0].tags).toBe("a b");
expect(result[0].text).toBe("Body text.");
expect(result[0].type).toBe("text/x-markdown");
});
it("converts YAML arrays for list fields to TW bracketed lists", function() {
var result = ds("---\ntags: [concept, multi word tag, simple]\n---\n\nbody",{});
expect(result[0].tags).toBe("concept [[multi word tag]] simple");
});
it("falls back to plain body when no frontmatter present", function() {
var result = ds("Just a body, no frontmatter.",{});
expect(result[0].text).toBe("Just a body, no frontmatter.");
expect(result[0].title).toBeUndefined();
});
it("falls back to plain body when frontmatter is malformed", function() {
var result = ds("---\nnot: [valid yaml: at all\n---\n\nbody",{});
// Malformed YAML still parses something; we just ensure body is set
expect(result[0].text).toBeDefined();
});
it("parses ISO-8601 created and modified into TW native format", function() {
var result = ds("---\ntitle: T\ncreated: 2025-01-02T03:04:05.006Z\nmodified: 2026-02-03T04:05:06Z\n---\n\nb",{});
expect(result[0].created).toBe("20250102030405006");
expect(result[0].modified).toBe("20260203040506000");
});
it("accepts a bare YYYY-MM-DD date for created/modified", function() {
var result = ds("---\ntitle: T\ncreated: 2025-03-15\n---\n\nb",{});
expect(result[0].created).toBe("20250315000000000");
});
it("passes through TW native timestamps for created/modified", function() {
var result = ds("---\ntitle: T\ncreated: \"20250101000000000\"\nmodified: \"20260101000000\"\n---\n\nb",{});
expect(result[0].created).toBe("20250101000000000");
expect(result[0].modified).toBe("20260101000000000");
});
it("drops unparseable created/modified values", function() {
var result = ds("---\ntitle: T\ncreated: not-a-date\n---\n\nb",{});
expect(result[0].created).toBeUndefined();
});
it("merges existing tags with frontmatter tags", function() {
var result = ds("---\ntags: [b, c]\n---\n\nbody",{tags: "a"});
// Order: existing first, then new uniques
expect(result[0].tags).toBe("a b c");
});
it("emits non-string non-array values as JSON", function() {
var result = ds("---\ntitle: T\nmeta: {nested: deep}\n---\n\nb",{});
expect(result[0].meta).toBe('{"nested":"deep"}');
});
it("handles CRLF line endings around frontmatter", function() {
var result = ds("---\r\ntitle: T\r\n---\r\n\r\nbody",{});
expect(result[0].title).toBe("T");
expect(result[0].text).toBe("body");
});
});
// --- Serializer ---
describe("frontmatter serializer", function() {
var ser = serializer["text/x-markdown"];
it("emits frontmatter and body", function() {
var t = new $tw.Tiddler({title: "Foo", text: "body", tags: "a b"});
var out = ser(t);
expect(out).toContain("---\n");
expect(out).toContain("title: Foo");
expect(out).toContain("tags:\n - a\n - b");
expect(out.split("\n---\n\n")[1]).toBe("body");
});
it("emits list fields as YAML arrays preserving multi-word tags", function() {
var t = new $tw.Tiddler({title: "X", tags: "concept [[multi word tag]] simple", text: "b"});
var out = ser(t);
expect(out).toContain("- concept");
expect(out).toContain("- multi word tag");
expect(out).toContain("- simple");
});
it("skips text, bag, revision", function() {
var t = new $tw.Tiddler({
title: "X",
text: "body",
bag: "default",
revision: "1"
});
var out = ser(t);
expect(out).not.toContain("bag:");
expect(out).not.toContain("revision:");
expect(out).not.toContain("text:");
});
it("emits created and modified as ISO-8601 strings", function() {
var t = new $tw.Tiddler({
title: "X",
text: "b",
created: "20250102030405006",
modified: "20260203040506000"
});
var out = ser(t);
expect(out).toContain('created: "2025-01-02T03:04:05.006Z"');
expect(out).toContain('modified: "2026-02-03T04:05:06.000Z"');
});
it("drops unparseable created/modified values", function() {
var t = new $tw.Tiddler({title: "X", text: "b", created: "garbage"});
var out = ser(t);
expect(out).not.toContain("created:");
});
it("skips type when it equals text/x-markdown", function() {
var t = new $tw.Tiddler({title: "X", type: "text/x-markdown", text: "b"});
expect(ser(t)).not.toContain("type:");
});
it("emits type when it differs from text/x-markdown", function() {
var t = new $tw.Tiddler({title: "X", type: "text/html", text: "b"});
expect(ser(t)).toContain("type: text/html");
});
it("emits no frontmatter when only skipped fields are present", function() {
var t = new $tw.Tiddler({text: "body only"});
expect(ser(t)).toBe("body only");
});
it("returns empty string for null tiddler", function() {
expect(ser(null)).toBe("");
});
it("title appears first in output", function() {
var t = new $tw.Tiddler({title: "Z", rating: "9", tags: "a", text: "b"});
var out = ser(t);
var lines = out.split("\n");
// First line is "---", second should be "title: Z"
expect(lines[0]).toBe("---");
expect(lines[1]).toBe("title: Z");
});
});
// --- Round-trip ---
describe("frontmatter round-trip", function() {
var ds = deserializer["text/x-markdown"];
var ser = serializer["text/x-markdown"];
it("preserves title, tags, and body across deserialize → serialize", function() {
var input = "---\ntitle: My Tiddler\ntags: [concept, synthesis]\nrating: \"7\"\n---\n\nThis is the body.";
var fields = ds(input,{})[0];
var t = new $tw.Tiddler(fields);
var out = ser(t);
var reparsed = ds(out,{})[0];
expect(reparsed.title).toBe("My Tiddler");
expect(reparsed.tags).toBe("concept synthesis");
expect(reparsed.rating).toBe("7");
expect(reparsed.text).toBe("This is the body.");
});
it("preserves created and modified across deserialize → serialize", function() {
var input = "---\ntitle: T\ncreated: 2025-01-02T03:04:05.006Z\nmodified: 2026-02-03T04:05:06.007Z\n---\n\nbody";
var fields = ds(input,{})[0];
var t = new $tw.Tiddler(fields);
var out = ser(t);
var reparsed = ds(out,{})[0];
expect(reparsed.created).toBe("20250102030405006");
expect(reparsed.modified).toBe("20260203040506007");
});
});
});
@@ -0,0 +1,60 @@
/*\
title: test-parsetree-positions.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Regression tests for #9882: wikitext parser rules must emit accurate
`start`/`end` source positions on their parse tree nodes. Tooling that maps
rendered output back to the source text relies on these offsets.
\*/
"use strict";
describe("Parse tree source position tests (#9882)", function() {
// Create a wiki
var wiki = $tw.test.wiki();
// Define a parsing shortcut
var parse = function(text) {
return wiki.parseText("text/vnd.tiddlywiki",text).tree;
};
it("should give inline code runs a text node that spans only the code, not the backticks", function() {
// codeinline.js: `code` gives a text node "code" spanning offsets 1 to 5. The closing backtick at 5 is excluded.
// Bug: `end` was set to `this.parser.pos`, which sits past the closing backtick, so end was 6 and the span swallowed the backtick.
expect(parse("`code`")).toEqual(
[ { type: "element", tag: "p", rule: "parseblock", start: 0, end: 6, children: [ { type: "element", tag: "code", rule: "codeinline", start: 0, end: 6, children: [ { type: "text", text: "code", start: 1, end: 5 } ] } ] } ]
);
// ``a`b`` gives text "a`b" spanning 2 to 5. `end` must be the offset of the closing marker whatever its length.
// Bug: `end` was `this.parser.pos` (7), two characters past the code, so it also swallowed the closing ``.
expect(parse("``a`b``")).toEqual(
[ { type: "element", tag: "p", rule: "parseblock", start: 0, end: 7, children: [ { type: "element", tag: "code", rule: "codeinline", start: 0, end: 7, children: [ { type: "text", text: "a`b", start: 2, end: 5 } ] } ] } ]
);
});
it("should start the text node of a suppressed external link after the ~", function() {
// extlink.js: ~https://example.com/ emits the plain text "https://example.com/", which spans offsets 1 to 21.
// Bug: `start` was the offset of the ~ (0), so the span was one character too wide and began on the ~ that the text omits.
expect(parse("~https://example.com/")).toEqual(
[ { type: "element", tag: "p", rule: "parseblock", start: 0, end: 21, children: [ { type: "text", text: "https://example.com/", start: 1, end: 21, rule: "extlink" } ] } ]
);
});
it("should give a suppressed wikilink's text node source positions", function() {
// wikilinkprefix.js: ~SuppressedLink emits the plain text "SuppressedLink", spanning offsets 1 to 15.
// Bug: the text node carried no `start`/`end` at all. The parser framework then defaulted `start` to the ~ offset (0).
expect(parse("~SuppressedLink")).toEqual(
[ { type: "element", tag: "p", rule: "parseblock", start: 0, end: 15, children: [ { type: "text", text: "SuppressedLink", start: 1, end: 15, rule: "wikilinkprefix" } ] } ]
);
});
it("should record the filter's start offset for an \\import pragma", function() {
// import.js: \import [tag[x]] records the filter value "[tag[x]]" starting at offset 8, right after "\import ".
// Bug: `filterStart` was assigned `this.parser.source` (the whole source string) instead of `this.parser.pos`, so `start` was a string, not an offset.
expect(parse("\\import [tag[x]]\n")).toEqual(
[ { type: "importvariables", rule: "import", start: 0, end: 16, attributes: { filter: { type: "string", value: "[tag[x]]", start: 8, end: 16 } }, children: [] } ]
);
});
});
@@ -0,0 +1,144 @@
/*\
title: test-select-widget.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests the select widget, focused on multi-select refresh behaviour.
\*/
"use strict";
describe("Select widget", function() {
var widget = require("$:/core/modules/widgets/widget.js");
// Helpers reused from: test-widget.js and test-checkbox-widget.js
function createWidgetNode(parseTreeNode,wiki) {
return new widget.widget(parseTreeNode,{
wiki: wiki,
document: $tw.fakeDocument
});
}
function parseText(text,wiki,options) {
var parser = wiki.parseText("text/vnd.tiddlywiki",text,options);
return parser ? {type: "widget", children: parser.tree} : undefined;
}
function renderWidgetNode(widgetNode) {
$tw.fakeDocument.setSequenceNumber(0);
var wrapper = $tw.fakeDocument.createElement("div");
widgetNode.render(wrapper,null);
return wrapper;
}
function refreshWidgetNode(widgetNode,wrapper,changes) {
var changedTiddlers = {};
if(changes) {
$tw.utils.each(changes,function(title) {
changedTiddlers[title] = true;
});
}
widgetNode.refresh(changedTiddlers,wrapper,null);
}
// Don't pass fakedom elements to Jasmine matchers. The matchers pretty-print
// values on assertion which walks the element.style Proxy with Symbol keys and
// crashes inside convertStyleNameToPropertyName.
// TODO: once issue: "fakedom style Proxy guards against non-string property keys" lands
// revert these workarounds to idiomatic matchers (toBe(null), toBeUndefined()).
function findSelectDom(node) {
if(node.tag === "select") return node;
if(node.children) {
for(var i = 0; i < node.children.length; i++) {
var found = findSelectDom(node.children[i]);
if(found) return found;
}
}
return null;
}
function selectedFlags(parent) {
var result = [];
for(var i = 0; i < parent.children.length; i++) {
result.push(!!parent.children[i].selected);
}
return result;
}
// Regression test for https://github.com/TiddlyWiki/TiddlyWiki5/issues/9839
// PR #8093 added <optgroup> support but used `child.children.length === 0` to
// distinguish a plain <option> from an <optgroup>. That heuristic misfires for
// any <option> whose contents render to inline HTML elements (e.g. tc-tiddlylink
// auto-links generated for "$:/..." titles), so the option's `selected` state
// was never restored on refresh.
it("preserves multi-select state across refresh when options contain inline HTML children",function() {
var wiki = $tw.test.wiki();
wiki.addTiddlers([
{title: "Picks", mylist: "foo $:/mumble"}
]);
// Each option has element children to mimic the auto-link case from #9839.
// Explicit value attribute lets fakedom resolve option.value (real browsers
// fall back to text content).
var widgetText = "<$select tiddler='Picks' field='mylist' multiple>" +
"<option value='foo'><span>foo</span></option>" +
"<option value='bar'><span>bar</span></option>" +
"<option value='$:/mumble'><a class='tc-tiddlylink'>$:/mumble</a></option>" +
"</$select>";
var widgetNode = createWidgetNode(parseText(widgetText,wiki),wiki);
var wrapper = renderWidgetNode(widgetNode);
var select = findSelectDom(wrapper);
expect(select === null).toBe(false);
expect(select.children.length).toBe(3);
// After initial render, options matching the field value should be selected.
// foo (idx 0), bar (idx 1), $:/mumble (idx 2).
expect(selectedFlags(select)).toEqual([true,false,true]);
// Change the stored field value and refresh - this is where the bug surfaced:
// the "$:/mumble" option (with an <a> child) was wrongly skipped.
wiki.addTiddler({title: "Picks", mylist: "bar $:/mumble"});
refreshWidgetNode(widgetNode,wrapper,["Picks"]);
expect(selectedFlags(select)).toEqual([false,true,true]);
// The inner <a> must not be touched - .selected is meaningful only on <option>.
var innerLinkSelected = select.children[2].children[0].selected;
expect(innerLinkSelected === undefined).toBe(true);
});
it("still selects options inside <optgroup> across refresh, including $:/-prefixed entries with inline HTML",function() {
var wiki = $tw.test.wiki();
wiki.addTiddlers([
{title: "Picks", mylist: "1 $:/mumble"}
]);
// The "high" group mixes a plain option with a $:/-prefixed option whose
// content is wrapped in an auto-link <a> - same pattern that broke #9839
// for top-level options, exercised here inside an <optgroup>.
var widgetText = "<$select tiddler='Picks' field='mylist' multiple>" +
"<optgroup label='low'>" +
"<option value='1'>1</option>" +
"<option value='2'>2</option>" +
"</optgroup>" +
"<optgroup label='high'>" +
"<option value='4'>4</option>" +
"<option value='$:/mumble'><a class='tc-tiddlylink'>$:/mumble</a></option>" +
"</optgroup>" +
"</$select>";
var widgetNode = createWidgetNode(parseText(widgetText,wiki),wiki);
var wrapper = renderWidgetNode(widgetNode);
var select = findSelectDom(wrapper);
expect(select === null).toBe(false);
expect(select.children.length).toBe(2);
// Initial state: "1" and "$:/mumble" selected
expect(selectedFlags(select.children[0])).toEqual([true,false]);
expect(selectedFlags(select.children[1])).toEqual([false,true]);
wiki.addTiddler({title: "Picks", mylist: "2 4"});
refreshWidgetNode(widgetNode,wrapper,["Picks"]);
expect(selectedFlags(select.children[0])).toEqual([false,true]);
expect(selectedFlags(select.children[1])).toEqual([true,false]);
});
});
@@ -0,0 +1,240 @@
/*\
title: test-utils-copyObjectPropertiesSafe.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests $tw.utils.copyObjectPropertiesSafe, the root cause of #9869.
$eventcatcher serialises DOM events via JSON.stringify(copyObjectPropertiesSafe(event)).
The original bug was caused by instanceof Node/Window being realm-specific.
When an event originated from a different browser window, DOM objects from that
window were not detected and JSON.stringify() could throw "Illegal invocation".
The implementation must:
- skip DOM nodes and Window objects from other realms
- preserve normal event data
- preserve CustomEvent.detail payloads
- continue copying enumerable properties from non-DOM objects
- preserve arrays and break circular references
The tests use substitutes for foreign DOM objects because headless Node does not
provide a second browser realm. These objects model the important characteristics:
DOM nodes have nodeType/nodeName, and Window objects have window/self/document.
\*/
"use strict";
describe("copyObjectPropertiesSafe (#9869)", function() {
var cops = $tw.utils.copyObjectPropertiesSafe;
// Simulates a DOM node from another realm.
// The important characteristics are:
// - nodeType/nodeName identify it as a DOM node
// - circular parentNode references resemble real DOM trees
function fakeElement(tagName,extra) {
var node = $tw.utils.extend({
nodeType: 1,
nodeName: tagName,
tagName: tagName
},extra || {});
node.parentNode = node;
return node;
}
// Simulates a Window object from another realm.
function fakeWindow() {
var win = {
document: {},
location: {}
};
win.window = win;
win.self = win;
return win;
}
// An event carrying foreign DOM nodes and a Window.
function fakeEvent(type,target,extra) {
return $tw.utils.extend({
type: type,
target: target,
currentTarget: target,
view: fakeWindow(),
detail: 0,
isTrusted: true
},extra || {});
}
it("does not throw serialising any event type from a secondary window", function() {
var events = [
fakeEvent("focusin",fakeElement("INPUT")),
fakeEvent("change",fakeElement("SELECT")),
fakeEvent("click",fakeElement("BUTTON"),{
button: 0,
clientX: 5,
clientY: 9,
relatedTarget: null
}),
fakeEvent("mouseover",fakeElement("DIV"),{
relatedTarget: fakeElement("SPAN")
})
];
events.forEach(function(event) {
expect(function() {
JSON.stringify(cops(event));
}).not.toThrow();
});
});
it("drops DOM nodes and Window but keeps serialisable event data", function() {
var event = fakeEvent("click",fakeElement("BUTTON"),{
button: 0,
clientX: 5,
clientY: 9
});
var result = JSON.parse(JSON.stringify(cops(event)));
expect(result.target).toBeUndefined();
expect(result.currentTarget).toBeUndefined();
expect(result.view).toBeUndefined();
expect(result.type).toBe("click");
expect(result.detail).toBe(0);
expect(result.isTrusted).toBe(true);
expect(result.button).toBe(0);
expect(result.clientX).toBe(5);
expect(result.clientY).toBe(9);
});
it("preserves nested objects, arrays and circular reference handling", function() {
var event = fakeEvent("custom",fakeElement("DIV"),{
detail: {
nested: {
a: 1,
b: [2,3]
}
},
path: [
fakeElement("DIV"),
fakeWindow(),
42
]
});
event.self = event;
var result = JSON.parse(JSON.stringify(cops(event)));
expect(result.detail).toEqual({
nested: {
a: 1,
b: [2,3]
}
});
expect(result.path).toEqual([
null,
null,
42
]);
expect(result.self).toBeUndefined();
});
it("preserves CustomEvent.detail objects including custom object instances", function() {
function DetailObject() {
this.name = "example";
this.values = [1,2,3];
}
var event = fakeEvent("custom",fakeElement("DIV"),{
detail: new DetailObject()
});
var result = JSON.parse(JSON.stringify(cops(event)));
expect(result.detail).toEqual({
name: "example",
values: [1,2,3]
});
});
it("preserves CustomEvent.detail objects while dropping DOM objects inside them", function() {
var event = fakeEvent("custom",fakeElement("DIV"),{
detail: {
value: 123,
nested: {
target: fakeElement("SPAN"),
kept: "yes"
},
items: [
fakeElement("BUTTON"),
42
]
}
});
var result = JSON.parse(JSON.stringify(cops(event)));
expect(result.detail.value).toBe(123);
expect(result.detail.nested.target).toBeUndefined();
expect(result.detail.nested.kept).toBe("yes");
expect(result.detail.items).toEqual([
null,
42
]);
});
it("accepts primitive, array and object arguments", function() {
var nullProto = Object.create(null);
nullProto.kept = true;
expect(cops(42)).toBe(42);
expect(cops(null)).toBe(null);
expect(cops([
1,
"two",
{three: 3}
])).toEqual([
1,
"two",
{three: 3}
]);
expect(cops({
a: 1,
b: {
c: 2
}
})).toEqual({
a: 1,
b: {
c: 2
}
});
expect(JSON.parse(JSON.stringify(cops({
nullProto: nullProto
})))).toEqual({
nullProto: {
kept: true
}
});
});
});
+2 -1
View File
@@ -3,7 +3,8 @@
"plugins": [
"tiddlywiki/jasmine",
"tiddlywiki/wikitext-serialize",
"tiddlywiki/geospatial"
"tiddlywiki/geospatial",
"tiddlywiki/markdown"
],
"themes": [
"tiddlywiki/vanilla",
+4 -2
View File
@@ -1,14 +1,16 @@
created: 20231005205623086
modified: 20250807100434131
modified: 20260710090951727
tags: About
title: TiddlyWiki Archive
type: text/vnd.tiddlywiki
\procedure versions()
5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9
5.1.10 5.1.11 5.1.12 5.1.13 5.1.14 5.1.15 5.1.16 5.1.17 5.1.18 5.1.19
5.1.20 5.1.21 5.1.22 5.1.23
5.2.0 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7
5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.3.8 5.4.0
5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.3.8
5.4.0 5.4.1
\end
Older versions of TiddlyWiki are available in the [[archive|https://github.com/TiddlyWiki/tiddlywiki.com-gh-pages/tree/master/archive]]:
@@ -1,9 +1,9 @@
title: Community Survey 2025
created: 20250708130030654
modified: 20250826162904085
title: Community Survey 2025
<div style.float="right" style.padding-left="1em">
<$image source="Community Survey 2025" alt="Shaping the future of TiddlyWiki with the Community Survey 2025" width="280"/>
<$image source="Community Survey 2025 Image" alt="Shaping the future of TiddlyWiki with the Community Survey 2025" width="280"/>
</div>
The core developers work hard year by year to continuously improve ~TiddlyWiki. Part of the satisfaction is that we are not just building software for ourselves, were serving the needs of a wider community of users.
@@ -1,7 +1,7 @@
created: 20130822170200000
icon: $:/core/icon
list: [[A Gentle Guide to TiddlyWiki]] [[Discover TiddlyWiki]] [[Some of the things you can do with TiddlyWiki]] [[Ten reasons to switch to TiddlyWiki]] Examples [[What happened to the original TiddlyWiki?]]
modified: 20260420192600833
modified: 20260710091122803
tags: Welcome
title: HelloThere
type: text/vnd.tiddlywiki
@@ -1,5 +1,5 @@
created: 20150414070451144
list: [[HelloThumbnail - Donations]] [[HelloThumbnail - Newsletter]] [[HelloThumbnail - Community Survey 2025]] [[HelloThumbnail - Introduction Video]] [[HelloThumbnail - Grok TiddlyWiki]] [[HelloThumbnail - Latest Version]] [[HelloThumbnail - MultiWikiServer]] [[HelloThumbnail - Twenty Years of TiddlyWiki]] [[HelloThumbnail - TiddlyWiki Privacy]] [[HelloThumbnail - Marketplace]] [[HelloThumbnail - Intertwingled Innovations]] [[HelloThumbnail - TiddlyWikiLinks]]
list: [[HelloThumbnail - Latest Version]] [[HelloThumbnail - Donations]] [[HelloThumbnail - Newsletter]] [[HelloThumbnail - Community Survey 2025]] [[HelloThumbnail - Introduction Video]] [[HelloThumbnail - Grok TiddlyWiki]] [[HelloThumbnail - MultiWikiServer]] [[HelloThumbnail - Twenty Years of TiddlyWiki]] [[HelloThumbnail - TiddlyWiki Privacy]] [[HelloThumbnail - Marketplace]] [[HelloThumbnail - Intertwingled Innovations]] [[HelloThumbnail - TiddlyWikiLinks]]
modified: 20150414070948246
title: HelloThumbnail
type: text/vnd.tiddlywiki
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Some files were not shown because too many files have changed in this diff Show More