Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

## Unreleased

### Bug Fixes

- fix: JavaScript-escape anchor IDs and audio paths so values containing `"`, `\`, `<`, or newlines no longer break the generated init script.
- fix: Warn (once per render) when the bundled default `ding.mp3` cannot be located alongside the extension.
- fix: HTML-escape the button text so markup in the `text` argument is rendered as plain text rather than dropped or interpreted.

### New Features

- feat: Add `volume` attribute that clamps to the range [0.0, 1.0] and warns on out-of-range or non-numeric input.
- feat: Add `loop-audio` attribute that genuinely disables looping (works around Elevator.js's `setAttribute('loop', 'false')` no-op).
- feat: Add built-in named sounds. `audio=ding` and `end=ding` resolve to the bundled `ding.mp3`.
- feat: Add `shortcut` attribute to trigger the elevator from anywhere outside form fields via a keyboard key.
- feat: Add document-level disable via `extensions.elevator.enabled: false` in YAML metadata.

### Documentation

- docs: Document new attributes (`volume`, `loop-audio`, `shortcut`), built-in sounds, and global disable in `README.md`, `example.qmd`, `_schema.yml`, and `_snippets.json`.

### Refactoring

- refactor: Add `escape_js_string` helper to the shared `_modules/string.lua` module.
- refactor: Sync `_modules/` doc-style metadata with the canonical module headers and ship `_modules/logging.lua` alongside `string.lua` and `html.lua`.

## 1.4.0 (2026-03-23)

### Refactoring
Expand Down
68 changes: 58 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,67 @@ If you're using version control, you will want to check in this directory.

## Usage

To add an elevator button, use the `{{< elevator >}}` shortcode. For example:
To add an elevator button, use the `{{< elevator >}}` shortcode.

- Mandatory:
### Minimal

```markdown
{{< elevator >}}
```
```markdown
{{< elevator >}}
```

### Custom text and scroll target

The first positional argument is the button label and the second is the id of an element to scroll to instead of the top of the page.

```markdown
{{< elevator "Back to header" my-header >}}
```

### Audio

`audio` is the looping music played during the scroll and `end` is the sound played once the scroll finishes.

```markdown
{{< elevator audio=music.mp3 end=ding.mp3 >}}
```

The built-in name `ding` resolves to the bundled `ding.mp3`, so `audio=ding` or `end=ding` works without copying the file into your project.

### Volume

`volume` accepts a number in the range [0.0, 1.0]. Out-of-range or non-numeric values are clamped and a warning is emitted at render time.

```markdown
{{< elevator audio=music.mp3 volume=0.5 >}}
```

### Loop control

- Optional `<text-button>`, `<anchor-target>`, and `<audio=audio.mp3>`:
`loop-audio` defaults to `true` (Elevator.js's own default). Set it to `false` to play the main audio just once. The extension overrides Elevator.js's internal `setAttribute('loop', 'false')` no-op so this option actually disables looping.

```markdown
{{< elevator audio=music.mp3 loop-audio=false >}}
```

```markdown
{{< elevator <text-button> <anchor-target> <audio=audio.mp3> >}}
```
### Keyboard shortcut

`shortcut` binds a single key (matched against `KeyboardEvent.key`) that triggers the elevator from anywhere on the page, except when the focus is inside an `<input>`, `<textarea>`, `<select>`, or any `contenteditable` element.

```markdown
{{< elevator audio=music.mp3 shortcut="t" >}}
```

### Global disable

Set `extensions.elevator.enabled: false` in the document YAML to suppress every `{{< elevator >}}` shortcode in that document.

```yaml
---
extensions:
elevator:
enabled: false
---
```

## Example

Expand All @@ -40,7 +88,7 @@ Output of `example.qmd`:

---

[BossaBossa](_extensions/elevator/BossaBossa.mp3) by Kevin MacLeod | <https://incompetech.com/>.
[BossaBossa](BossaBossa.mp3) by Kevin MacLeod | <https://incompetech.com/>.
Music promoted by <https://www.chosic.com/free-music/all/>.
Creative Commons Creative Commons: By Attribution 3.0 License (<http://creativecommons.org/licenses/by/3.0/>).

Expand Down
2 changes: 1 addition & 1 deletion _extensions/elevator/_modules/html.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
--- MC HTML - HTML generation and dependency management for Quarto Lua filters and shortcodes
--- @module html
--- @module "html"
--- @license MIT
--- @copyright 2026 Mickaël Canouil
--- @author Mickaël Canouil
Expand Down
62 changes: 62 additions & 0 deletions _extensions/elevator/_modules/logging.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--- MC Logging - Formatted log output for Quarto Lua filters and shortcodes
--- @module "logging"
--- @license MIT
--- @copyright 2026 Mickaël Canouil
--- @author Mickaël Canouil
--- @version 1.0.0

local M = {}

-- ============================================================================
-- LOGGING UTILITIES
-- ============================================================================

--- Format and log an error message with extension prefix.
--- Provides standardised error messages with consistent formatting across extensions.
--- Format: [extension-name] Message with details.
---
--- @param extension_name string The name of the extension (e.g., "external", "lua-env")
--- @param message string The error message to display
--- @usage M.log_error("external", "Could not open file 'example.md'.")
function M.log_error(extension_name, message)
quarto.log.error('[' .. extension_name .. '] ' .. message)
end

--- Format and log a warning message with extension prefix.
--- Provides standardised warning messages with consistent formatting across extensions.
--- Format: [extension-name] Message with details.
---
--- @param extension_name string The name of the extension (e.g., "external", "lua-env")
--- @param message string The warning message to display
--- @usage M.log_warning("lua-env", "No variable name provided.")
function M.log_warning(extension_name, message)
quarto.log.warning('[' .. extension_name .. '] ' .. message)
end

--- Format and log an output message with extension prefix.
--- Provides standardised informational messages with consistent formatting across extensions.
--- Format: [extension-name] Message with details.
---
--- @param extension_name string The name of the extension (e.g., "lua-env")
--- @param message string The informational message to display
--- @usage M.log_output("lua-env", "Exported metadata to: output.json")
function M.log_output(extension_name, message)
quarto.log.output('[' .. extension_name .. '] ' .. message)
end

--- Format and log a debug message with extension prefix.
--- Provides standardised debug messages with consistent formatting across extensions.
--- Format: [extension-name] Message with details.
---
--- @param extension_name string The name of the extension (e.g., "lua-env")
--- @param message string The debug message to display
--- @usage M.log_debug("lua-env", "Variable 'x' has value: 42")
function M.log_debug(extension_name, message)
quarto.log.debug('[' .. extension_name .. '] ' .. message)
end

-- ============================================================================
-- MODULE EXPORT
-- ============================================================================

return M
23 changes: 22 additions & 1 deletion _extensions/elevator/_modules/string.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
--- MC String - String manipulation and escaping for Quarto Lua filters and shortcodes
--- @module string
--- @module "string"
--- @license MIT
--- @copyright 2026 Mickaël Canouil
--- @author Mickaël Canouil
Expand Down Expand Up @@ -112,6 +112,27 @@ function M.escape_typst_string(text)
return text:gsub('\\', '\\\\'):gsub('"', '\\"')
end

--- Escape characters for JavaScript string literals (inside `"..."` or `'...'`).
--- Handles backslash, both quote styles, newlines, carriage returns, tabs,
--- form feeds, and the `</` sequence so payloads cannot break out of a
--- surrounding inline `<script>` block.
--- @param text string|nil The text to escape
--- @return string The escaped text safe for JavaScript string literals
--- @usage local safe = M.escape_js_string([[a "b" </script>]])
function M.escape_js_string(text)
if text == nil then return '' end
if type(text) ~= 'string' then text = tostring(text) end
return text
:gsub('\\', '\\\\')
:gsub('"', '\\"')
:gsub("'", "\\'")
:gsub('\n', '\\n')
:gsub('\r', '\\r')
:gsub('\t', '\\t')
:gsub('\f', '\\f')
:gsub('</', '<\\/')
end

--- Escape special Lua pattern characters for use in string.gsub.
--- @param text string The text containing characters to escape
--- @return string The escaped text safe for Lua patterns
Expand Down
31 changes: 22 additions & 9 deletions _extensions/elevator/_schema.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
# _schema.yml for "elevator" shortcode extension
# Describes shortcode arguments and attributes for IDE tooling and runtime validation.
# Schema for the elevator extension
$schema: https://m.canouil.dev/quarto-wizard/assets/schema/v2/extension-schema.json

# Per-shortcode schemas.
# Describes positional arguments and named attributes for {{< elevator >}}.

$schema: https://m.canouil.dev/quarto-wizard/assets/schema/v1/extension-schema.json
options:
enabled:
type: boolean
default: true
description: "Whether {{< elevator >}} shortcodes render anything; set to false to disable every shortcode in the document."

shortcodes:
elevator:
description: "Creates a button that scrolls smoothly to the top of the page with elevator music sound effects."
arguments:
- name: text
type: string
description: "Text to display on the button. Defaults to 'Return to the top!'."
default: "Return to the top!"
description: "Text to display on the button."
- name: target
type: string
description: "Element ID to scroll to instead of the top of the page."
attributes:
audio:
type: string
description: "Path to the main audio file played during the scroll animation."
description: "Path to the main audio file played during the scroll animation; the built-in name 'ding' resolves to the bundled ding.mp3."
completion:
type: file
extensions:
Expand All @@ -30,10 +31,22 @@ shortcodes:
end:
type: string
default: "ding.mp3"
description: "Path to the audio file played when the scroll completes."
description: "Path to the audio file played when the scroll completes; the built-in name 'ding' resolves to the bundled ding.mp3."
completion:
type: file
extensions:
- .mp3
- .ogg
- .wav
volume:
type: number
minimum: 0.0
maximum: 1.0
description: "Playback volume in the range [0.0, 1.0]; values outside the range are clamped and a warning is emitted."
loop-audio:
type: boolean
default: true
description: "Whether the main audio loops while scrolling; set to false to play the audio only once."
shortcut:
type: string
description: "Optional keyboard key (matched against KeyboardEvent.key) that triggers the elevator from anywhere outside form fields, e.g. 't' or 'Escape'."
15 changes: 15 additions & 0 deletions _extensions/elevator/_snippets.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,20 @@
"prefix": "elevator",
"body": "{{< elevator \"${1:Back to top}\" >}}",
"description": "Insert an elevator (back to top) shortcode"
},
"Elevator shortcode with audio": {
"prefix": "elevator-audio",
"body": "{{< elevator \"${1:Back to top}\" audio=\"${2:music.mp3}\" >}}",
"description": "Insert an elevator shortcode with main audio"
},
"Elevator shortcode with target": {
"prefix": "elevator-target",
"body": "{{< elevator \"${1:Go up}\" \"${2:target-id}\" >}}",
"description": "Insert an elevator shortcode scrolling to a target id"
},
"Elevator shortcode with volume and shortcut": {
"prefix": "elevator-full",
"body": "{{< elevator \"${1:Back to top}\" audio=\"${2:music.mp3}\" volume=${3:0.5} loop-audio=${4:true} shortcut=\"${5:t}\" >}}",
"description": "Insert an elevator shortcode with volume, loop, and keyboard shortcut"
}
}
Loading