Skip to content

feat(editor): ${filename} in the image directory, so a folder can belong to one document (#714) - #716

Merged
PathGao merged 2 commits into
masterfrom
feat/image-directory-filename-variable
Aug 24, 2026
Merged

feat(editor): ${filename} in the image directory, so a folder can belong to one document (#714)#716
PathGao merged 2 commits into
masterfrom
feat/image-directory-filename-variable

Conversation

@PathGao

@PathGao PathGao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Second half of #714. The session-persistence half is a separate conversation and is not touched here.

imageDirectory is read literally, so every document in a folder shares one img/. A directory of notes accumulates one flat pile of screenshots with nothing saying which document any of them belongs to, and renaming or moving a document takes none of its images with it.

What changed

${filename} in that setting now expands to the document's name without its extension. ${filename}.assets next to notes/trip.md writes to notes/trip.assets; each document gets its own folder. A setting without the token is still the literal folder name it has always been, and an empty setting still means img, so nothing changes for anyone who does not type the token.

Three call sites read the setting — paste (save_image), drop (copy_file_to_img), and the path completion that lists the image folder for ![](… suggestions. All three go through one resolveImageDirectory now.

Choices

The token is spelled ${filename} because Typora and SoloMD spell it that way. This request comes from people migrating off those, and the string they already have in their config is ${filename}.assets. Inventing a different spelling would mean every one of them has to be told about it.

Rust is unchanged, and that is the safety argument, not a convenience. The expansion produces a path component, not a path: trip.assets still goes through safe_path_component, which refuses separators, ., .. and absolute paths, and resolve_image_directory still asserts the result stays inside the document's directory. That guard is also why this function special-cases no stem of its own — a document named ..md expands to . and Rust is the one that refuses it, rather than a second validator in TypeScript that could disagree with the first.

Flat templates only. ./images/${filename}/ would require relaxing that constraint to allow separators, which is a different change with a different review. ${filename}.assets — the form the issue asks for, and Typora's own default — needs none of it.

Substitution is split/join, not String.replace. The replacement is a filename the user chose, and replace reads $& and $' in a replacement string as backreferences: a document named $&.md would have created a folder called ${filename}.assets on disk, containing the literal token. Pinned by a test.

One function rather than three inline expansions. The completion site is the one that makes this more than style: expanding at paste and drop but not there would list a directory named after the literal token, which exists nowhere, so image-path suggestions would silently return nothing for exactly the users who enabled the feature. A source assertion now fails if a call site resolves settings.imageDirectory by hand again.

The hint is English-only. The other 25 locales fall back to English, which i18nCoverage.test.ts documents as the supported path — the alternative is that no English string can be added without 25 translations first. It rides on the input's title, the tooltip pattern FindBar.svelte already uses; Settings.svelte has no description-text pattern, and inventing the first one there is a UI decision for its own PR. The placeholder stays img on purpose: it describes what an empty setting does, and ${filename}.assets in that slot would say otherwise.

Not in this change

  • Rename. A renamed document leaves its assets folder behind under the old name. Typora moves the folder and rewrites the links; that is the larger half of per-document folders and needs its own design (what happens on a collision, on a partial move, on a folder the user has since edited).
  • Failure reporting. A rejected or failed image write still only reaches console.error, as it did before for a full disk or a read-only directory. Unchanged behavior, worth fixing separately.
  • Existing images. Nothing migrates; the setting applies to images written after it changes.

Tests

982 node --test green, svelte-check clean. New: two behavior tests over the expansion (Windows paths, the last-dot rule, .gitignore-style names, no-token and empty settings, and the $& case) and one source assertion that no call site resolves the setting inline.

PathGao added 2 commits August 25, 2026 04:41
…ong to one document (#714)

`imageDirectory` was taken literally, so every document in a folder shared one
`img/` and a directory of notes accumulated one flat pile of screenshots with
no way to tell which document any of them belonged to.

`${filename}` in that setting now expands to the document's name without its
extension: `${filename}.assets` next to `notes/trip.md` writes to
`notes/trip.assets`. The token is spelled the way Typora and SoloMD spell it,
because a user arriving from either types what already worked there.

The expansion is a path *component*, so Rust is unchanged: `trip.assets` still
goes through `safe_path_component`, which refuses separators, `.`, `..` and
absolute paths and keeps the folder inside the document's directory. That guard
is also the answer for the stems this does not special-case -- a file named
`..md` expands to `.` and Rust says no. A nested template (`./images/${filename}/`)
would need that constraint relaxed and is deliberately not part of this.

The three call sites that read the setting -- paste, drop, and the path
completion that lists the image folder -- go through one function now, and a
source assertion keeps the fourth from being written by hand: expanding at two
of them and not the completion would offer suggestions out of a folder named
after the literal token, which exists nowhere.

Substitution is `split`/`join`, not `String.replace`: the replacement is a
filename the user chose, and `replace` reads `$&` and `$'` in a replacement as
backreferences, so a document named `$&.md` would have put `${filename}.assets`
on disk verbatim.

Not changed: renaming a document still leaves its assets folder behind under
the old name (Typora moves it and rewrites the links), and a failed image write
still only reaches the console. Both are their own change.

The setting's hint is English-only; the other 25 locales fall back to it, which
i18nCoverage.test.ts allows by design.

Tests: 982 node, svelte-check clean.
imageUndoKeepsFile.spec.ts evaluates the component's own paste and drop
statements over a scope it builds by hand, so a name those statements newly
reference has to be added to the scope or it is simply undefined at call time.
`resolveImageDirectory` was not, and both handlers threw where they read the
image directory — inside the try/catch that exists for a failed disk write, so
the throw surfaced as "the drop did not insert an embed" rather than as a
ReferenceError.

It replaces `DEFAULT_IMAGE_DIRECTORY` in the parameter list rather than joining
it: the lifted code no longer names that constant, and a parameter nothing
reads is the next person's puzzle.
@PathGao
PathGao merged commit 54d7098 into master Aug 24, 2026
4 checks passed
@PathGao
PathGao deleted the feat/image-directory-filename-variable branch August 24, 2026 22:32
PathGao added a commit that referenced this pull request Aug 25, 2026
Four things shipped since 2.7.4 that the file which documents what Markpad can
do never heard about. It carries the editing behaviour around each construct,
not only the spellings, so each one belongs to a section that already exists.

- Lists: `Tab` moved a line by tabSize and left the marker alone. #713 makes it
  a level change -- the parent's content column, and both numbered lists
  renumbered -- which is what the file already claimed and now describes
  accurately.
- Quotes: `Enter` continues a block quote (#705), so the section gets the
  paragraph Lists has had. Including that one keystroke clears an empty quoted
  line at any depth, which is the way out.
- Images: where a pasted or dropped image lands, and `${filename}` in that
  setting (#716). It expands to a folder name, not a path, and the note says so
  -- `./images/${filename}/` is not a thing you can write here.
- Not-syntax: copying from the preview keeps its formatting (#680), and the
  split panes can trade sides (#693).

Tests: 984 pass.
alecdotdev pushed a commit that referenced this pull request Aug 25, 2026
…own again (#719)

* chore: bump version to 2.7.5

* docs(syntax): bring the reference up to 2.7.5, in both languages

Four things shipped since 2.7.4 that the file which documents what Markpad can
do never heard about. It carries the editing behaviour around each construct,
not only the spellings, so each one belongs to a section that already exists.

- Lists: `Tab` moved a line by tabSize and left the marker alone. #713 makes it
  a level change -- the parent's content column, and both numbered lists
  renumbered -- which is what the file already claimed and now describes
  accurately.
- Quotes: `Enter` continues a block quote (#705), so the section gets the
  paragraph Lists has had. Including that one keystroke clears an empty quoted
  line at any depth, which is the way out.
- Images: where a pasted or dropped image lands, and `${filename}` in that
  setting (#716). It expands to a folder name, not a path, and the note says so
  -- `./images/${filename}/` is not a thing you can write here.
- Not-syntax: copying from the preview keeps its formatting (#680), and the
  split panes can trade sides (#693).

Tests: 984 pass.

* docs(release): the download table's two warnings describe 2.7.5, not 2.7.4

The table is composed in `build.yml` and printed on every release page, so both
notes ship with whatever master holds when the workflow is dispatched. Both are
about to be wrong.

macOS: the last paragraph told users the app grants file access per prompt and
that self-signing in Keychain Access is the way out, redone after every update.
#707 is what that paragraph asks for, so it would print the workaround on the
first release that no longer needs it. Replaced with what is now true, worded so
it stays true for 2.7.6: the grant survives an update, and only a user coming
from a release older than 2.7.5 is asked once more, because the signature
changes the identity the old grants belonged to. The Gatekeeper paragraph above
it is untouched -- signing is not notarization, and the first-launch dialog is
unaffected.

Windows: the "false positive Trojan" half is the stale one -- VirusTotal no
longer flags the portable `.exe`, which is what #334 and #466 were. The
SmartScreen half is not: it fires on an unsigned binary regardless of what any
scanner says, and stays true until an Authenticode certificate exists (#562).
Dropping the whole note would leave the release page silent about a dialog every
Windows user still meets. So the antivirus claim goes and the unrecognized-app
one stays, in one shorter sentence.

Tests: 984 pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant