Skip to content

feat(macro): run a user script from a note's ```js code block (#1065)#1341

Open
chhoumann wants to merge 2 commits into
masterfrom
chhoumann/1065-userscript-in-codeblock
Open

feat(macro): run a user script from a note's ```js code block (#1065)#1341
chhoumann wants to merge 2 commits into
masterfrom
chhoumann/1065-userscript-in-codeblock

Conversation

@chhoumann

@chhoumann chhoumann commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Closes #1065.

What & why

A Macro user script currently must be a standalone .js file, which Obsidian can't open or edit natively — especially painful on mobile. This lets a user script live in a regular note instead: QuickAdd runs the first ```js (or ```javascript) fenced block and ignores the surrounding prose and any other code blocks.

Verified in the dev vault (QuickAdd 2.13.1) that the gap was real before this change: the picker's /\.js$/ filter excluded notes, and pointing a command at a .md fed raw markdown to new FunctionSyntaxError.

How

  • extractScriptFromMarkdown (new, pure, App-free line scanner): first js fence wins; CommonMark fence rules (a line with trailing content like console.log("```") doesn't close the block; wrap with 4+ backticks to embed a bare ```); CRLF preserved byte-for-byte; empty-vs-missing fence distinguished; leading blank-line padding keeps runtime error line numbers near the note's own.
  • getUserScript branches on .md and feeds the extracted body to the same CommonJS Function shim. So module.exports / exports.default / settings / entry / name::member and all five getUserScript callers (macro command, conditional script-condition, ::member, single-macro entry, and the one-page preflight scanner) work unchanged. The .js path is byte-identical → zero regression. A note with no runnable fence shows a Notice and the macro continues (no silent failure, no crash).
  • Script pickers (Macro Builder + Conditional modal): one unified list of .js files and notes-that-have-a-code-block (cheap metadata-cache pre-filter), with a validate-on-select backstop that reads the note and reports the precise reason if there's no runnable fence. Notes resolve by path so a bare basename never picks a note over a same-named .js; a direct-vault fallback covers a cold cache.
  • Package transparency (packagePreview): a bundled .md whose content has a runnable js fence is now treated as executable, closing a disclosure-gate bypass (an orphan note labeled template would otherwise slip the gate, land on disk, and run via any macro pointing at its path). Plain .md templates are not flagged.
  • Package import (packageImportService): a note script's command name (= its vault path, optionally path::member) is kept in sync when the asset is written to a new destination on import.

Design & review

Designed and reviewed before implementation (gap confirmed live; design challenged by an ultracode multi-lens pass + adversarial reviewers). Decisions: unified single picker; no-fence ⇒ Notice + continue; precise decode-and-extract for the transparency gate. The implementation diff was then put through a second adversarial review — its findings (typed-resolution basename collision, import name staleness, and not documenting the feature in the stable 2.13.1 docs snapshot) are all addressed here.

Tests

  • extractScriptFromMarkdown.test.ts — 16 cases (selection rule, fence edges, CRLF, frontmatter, callout/tilde rejected, empty vs missing, padding).
  • getUserScript.md load + ::member + no-fence Notice + non-module.exports body.
  • scriptCandidates.test.ts — resolver picks .js over a same-named note; path/cold-cache resolution; validate-on-select reasons.
  • packagePreview.test.ts — orphan .md with a js fence is critical/review-required; plain .md template is not.
  • packageImportService.test.ts — note-script name tracks a remapped asset path.
  • Full suite green (1976 passing); tsc + ESLint clean; production build OK; docs build (Next) OK.

Verification (dev vault, real Obsidian)

  • A macro pointing at a note ran its ```js block; the side effect persisted and the decoy ```python block was correctly ignored.
  • A no-fence note was graceful — the macro continued to its next command, no error thrown.

Notes

  • main.js / styles.css are gitignored here and built in CI/release, so they're not in this diff.
  • Feature docs are in docs/docs/ (Next) only; they'll snapshot into the version docs when the next release is cut (documenting it in the stable 2.13.1 snapshot would claim support that release doesn't have).

Summary by CodeRabbit

  • New Features
    • User scripts can now be stored either as standalone .js files or inside a note’s first js/javascript code block.
    • Script pickers and prompts now recognize both formats with clearer labels and guidance.
    • Imported packages keep note-based script references in sync when script destinations change.
  • Bug Fixes
    • Improved detection and user-facing messaging when no runnable script block is found.
    • Package import preview more accurately flags markdown notes that contain executable JavaScript.
  • Documentation
    • Updated user guidance for placing and referencing note-embedded scripts and macro script commands.

User scripts can now live in a Markdown note instead of only a standalone
.js file — handy on mobile, where Obsidian can't open .js files. QuickAdd
runs the first ```js (or ```javascript) fence in the note and ignores the
surrounding prose and any other code blocks.

- extractScriptFromMarkdown: a pure CommonMark-aware line scanner (first js
  fence wins; trailing-content lines don't close the block; CRLF preserved;
  leading blank-line padding keeps error line numbers near the note's).
- getUserScript branches on .md and feeds the extracted body to the same
  CommonJS Function shim, so exports/settings/entry/::member and all five
  callers (macro command, conditional script, member access, single-macro
  entry, preflight) work unchanged. The .js path stays byte-identical. A
  note with no runnable fence shows a Notice and returns undefined.
- Script pickers (macro builder + conditional modal) offer a unified list of
  .js files and notes-with-a-code-block (metadata-cache pre-filter) and
  validate the fence on select. Notes resolve by path so a bare basename
  never picks a note over a same-named .js.
- packagePreview: a bundled .md whose content has a runnable js fence is now
  treated as executable, closing a disclosure-gate bypass for note scripts.
- packageImportService: keep a note script's command name (= its path) in
  sync when the asset is written to a new destination on import.

Docs updated (Next). Verified in the dev vault: a note script runs with a
persisted side effect (decoy ```python block ignored) and a no-fence note
is graceful (macro continues).
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba63a634-19e9-42aa-8a17-8a96f886c321

📥 Commits

Reviewing files that changed from the base of the PR and between 404b2cd and 6202dc2.

📒 Files selected for processing (3)
  • docs/docs/Choices/MacroChoice.md
  • src/gui/MacroGUIs/CommandSequenceEditor.ts
  • src/gui/MacroGUIs/ConditionalCommandSettingsModal.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/docs/Choices/MacroChoice.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/gui/MacroGUIs/CommandSequenceEditor.ts
  • src/gui/MacroGUIs/ConditionalCommandSettingsModal.ts

📝 Walkthrough

Walkthrough

This PR adds support for user scripts defined in markdown note code fences, updates macro editing and condition UI to discover and validate those scripts, extends package preview/import logic for note-backed scripts, and refreshes tests and documentation to describe the new script formats and selection rules.

Changes

Note-backed user scripts

Layer / File(s) Summary
Markdown script extraction and execution
src/utils/extractScriptFromMarkdown.ts, src/utilityObsidian.ts, src/utils/extractScriptFromMarkdown.test.ts, src/utilityObsidian.test.ts
Adds CommonMark-style fenced-JavaScript extraction for markdown notes with line-number preservation and CRLF handling, uses extracted source in getUserScript(), shows a Notice when no runnable block exists, and covers fence parsing, member access, and note-backed execution in comprehensive tests.
Script candidate discovery and validation
src/gui/MacroGUIs/scriptCandidates.ts, src/gui/MacroGUIs/scriptCandidates.test.ts
Builds a unified ScriptCandidate model that discovers both .js files and markdown notes with code blocks, prioritizes path matching and falls back to direct vault lookup, validates markdown-backed scripts at selection time by extracting and checking for runnable code, and labels candidates per type for UI display.
Macro UI script picker integration
src/gui/MacroGUIs/CommandSequenceEditor.ts, src/gui/MacroGUIs/ConditionalCommandSettingsModal.ts
Updates macro and conditional-script pickers to load candidates, resolve user input selectors with validation, present unified candidate lists keyed by label and path, validate markdown selections before confirming, and store command names/paths appropriately for .js basenames or note vault paths.
Notice and test message updates
src/gui/MacroGUIs/noScriptsFoundNotice.ts, src/gui/apiModernization.test.ts
Replaces "JavaScript files" references with broader "scripts" terminology and adds both .js files and markdown code blocks to guidance, reflecting the expanded script source model.
Package preview and import path rewrites
src/services/packagePreview.ts, src/services/packageImportService.ts, src/services/packagePreview.test.ts, src/services/packageImportService.test.ts
Package preview decodes bundled markdown assets and flags those containing executable JavaScript fences for review; import path overrides now also rewrite note-backed UserScript command names when the underlying note path is remapped.
User script documentation
docs/docs/UserScripts.md, docs/docs/Choices/MacroChoice.md
Documents note-based user scripts, first-fence-only extraction behavior, CommonJS export requirements, supported fence syntax (including indentation and YAML frontmatter rules), browse and manual selector formats with :: member-access notation, and placement restrictions excluding .obsidian and hidden folders.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MacroEditor as Macro UI
  participant Candidates as loadScriptCandidates
  participant Validator as noteScriptError
  participant Vault
  participant Extractor as extractScriptFromMarkdown

  User->>MacroEditor: Type or browse script
  MacroEditor->>Candidates: discover candidates
  Candidates-->>MacroEditor: js files + markdown notes
  MacroEditor->>MacroEditor: resolve selector
  MacroEditor->>Validator: validate if markdown
  Validator->>Vault: read note
  Vault-->>Validator: content
  Validator->>Extractor: extract first js fence
  Extractor-->>Validator: code or error
  Validator-->>MacroEditor: ok or show notice
  MacroEditor-->>User: save UserScript path/name
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • chhoumann/quickadd#1320: Both PRs modify src/utilityObsidian.ts user-script loading to support extraction and execution of scripts from different sources.
  • chhoumann/quickadd#1305: Both PRs extend src/services/packagePreview.ts to detect executable content in bundled assets and update review gating logic.

Suggested labels

released

Poem

🐇 I found a script inside a note,
tucked in backticks like a tiny boat.
It hopped through macros, neat and bright,
with paths and fences checked just right.
Now markdown whispers, "Run me too!"
A rabbit's dream has come so true! 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main feature: enabling user scripts to run from JavaScript code blocks within Markdown notes, which is the core change throughout this PR.
Linked Issues check ✅ Passed The PR fully implements the feature requested in #1065: extracting and executing the first ```js code block from notes while ignoring surrounding content, plus unified script picker UI and proper path resolution.
Out of Scope Changes check ✅ Passed All changes align with the feature scope: markdown extraction, script picking/resolution, package preview/import handling, and comprehensive test coverage for note-based scripts.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chhoumann/1065-userscript-in-codeblock

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 13, 2026

Copy link
Copy Markdown

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6202dc2
Status: ✅  Deploy successful!
Preview URL: https://85b08a4f.quickadd.pages.dev
Branch Preview URL: https://chhoumann-1065-userscript-in.quickadd.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 404b2cd9a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/gui/MacroGUIs/ConditionalCommandSettingsModal.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/docs/Choices/MacroChoice.md`:
- Around line 83-86: Update the QuickAdd macro docs in MacroChoice.md so the
fence-language guidance matches runtime support: the section around the “first
code block runs” wording should mention both `js` and `javascript` fences. Use
the existing “Create a new script file…” paragraph and revise it to say the
first matching JavaScript fence runs, so users are not misled about supported
note formats.

In `@src/gui/MacroGUIs/CommandSequenceEditor.ts`:
- Around line 95-97: Refresh script candidates at interaction time instead of
caching them only in CommandSequenceEditor’s initialization: update the
CommandSequenceEditor flow so the resolve path and picker path both reload
scriptCandidates immediately before use, and apply the same stale-data pattern
fix in ConditionalCommandSettingsModal as well. Use the existing
loadScriptCandidates, the script resolution logic around command selection, and
the script picker population logic as the touchpoints so interactive macro
commands always reflect the current vault/index state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1f002c80-a752-42d2-87c7-c4d333932ed3

📥 Commits

Reviewing files that changed from the base of the PR and between c2ee449 and 404b2cd.

📒 Files selected for processing (16)
  • docs/docs/Choices/MacroChoice.md
  • docs/docs/UserScripts.md
  • src/gui/MacroGUIs/CommandSequenceEditor.ts
  • src/gui/MacroGUIs/ConditionalCommandSettingsModal.ts
  • src/gui/MacroGUIs/noScriptsFoundNotice.ts
  • src/gui/MacroGUIs/scriptCandidates.test.ts
  • src/gui/MacroGUIs/scriptCandidates.ts
  • src/gui/apiModernization.test.ts
  • src/services/packageImportService.test.ts
  • src/services/packageImportService.ts
  • src/services/packagePreview.test.ts
  • src/services/packagePreview.ts
  • src/utilityObsidian.test.ts
  • src/utilityObsidian.ts
  • src/utils/extractScriptFromMarkdown.test.ts
  • src/utils/extractScriptFromMarkdown.ts

Comment thread docs/docs/Choices/MacroChoice.md
Comment thread src/gui/MacroGUIs/CommandSequenceEditor.ts
…resh script candidates on interaction, docs note ```javascript fences
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.

[FEATURE REQUEST] Allow User Script to be in Note code block

1 participant