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
192 changes: 192 additions & 0 deletions great_docs/_mock_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
from __future__ import annotations

import re
from pathlib import Path

# Matches the opening fence of an executable code block:
# ```{python} or ```{python} (with trailing whitespace)
_EXEC_FENCE_RE = re.compile(r"^```\{python\}\s*$")

# Matches the closing fence:
# ```
_CLOSE_FENCE_RE = re.compile(r"^```\s*$")

# Matches a hash-pipe option line:
# #| key: value
_HASHPIPE_RE = re.compile(r"^#\|\s*(\S+?):\s*(.*)$")

# The delimiter that separates display code from eval code:
_DELIMITER = "# ---"


def expand_mock_cells(text: str) -> str:
"""Rewrite `source-code: mock` cells in *text* into two-cell pairs.

Parameters
----------
text
The full content of a `.qmd` file.

Returns
-------
str
The rewritten content. Unchanged if no mock cells are found.
"""
lines = text.split("\n")
out: list[str] = []
i = 0

while i < len(lines):
line = lines[i]

# Look for an opening executable fence
if not _EXEC_FENCE_RE.match(line):
out.append(line)
i += 1
continue

# Collect the entire cell (fence-to-fence)
cell_start = i
i += 1
cell_body: list[str] = []
found_close = False
while i < len(lines):
if _CLOSE_FENCE_RE.match(lines[i]):
found_close = True
i += 1
break
cell_body.append(lines[i])
i += 1

if not found_close:
# Malformed cell — emit as-is
out.append(lines[cell_start])
out.extend(cell_body)
continue

# Parse hash-pipe options from the top of the cell body
options: dict[str, str] = {}
option_lines: list[str] = []
body_start = 0
for j, bline in enumerate(cell_body):
m = _HASHPIPE_RE.match(bline)
if m:
options[m.group(1)] = m.group(2).strip()
option_lines.append(bline)
body_start = j + 1
else:
break

# Not a mock cell — emit unchanged
if options.get("source-code") != "mock":
out.append(lines[cell_start])
out.extend(cell_body)
out.append("```")
continue

# Split the remaining body at the delimiter
raw_body = cell_body[body_start:]
display_lines: list[str] = []
eval_lines: list[str] = []
found_delim = False
for bline in raw_body:
if not found_delim and bline.strip() == _DELIMITER:
found_delim = True
continue
if found_delim:
eval_lines.append(bline)
else:
display_lines.append(bline)

# Collect options to forward (everything except source-code)
# output-title and output-frame are forwarded to the eval cell only
output_title = options.pop("output-title", None)
output_frame = options.pop("output-frame", None)
# Remove source-code from forwarded options
options.pop("source-code", None)

# Build forwarded option lines for both cells
forwarded = []
for key, val in options.items():
forwarded.append(f"#| {key}: {val}")

# --- Emit the display cell (eval: false) ---
out.append("```{python}")
out.append("#| eval: false")
for fwd in forwarded:
# Don't forward echo/output overrides to display cell
if not fwd.startswith("#| echo:") and not fwd.startswith("#| output:"):
out.append(fwd)
out.extend(display_lines)
out.append("```")

# --- Emit the eval cell (echo: false) ---
if found_delim and eval_lines:
out.append("")
out.append("```{python}")
out.append("#| echo: false")
if output_title:
out.append(f"#| output-title: {output_title}")
if output_frame:
out.append(f"#| output-frame: {output_frame}")
for fwd in forwarded:
# Don't forward eval overrides to eval cell
if not fwd.startswith("#| eval:"):
out.append(fwd)
out.extend(eval_lines)
out.append("```")
elif not found_delim:
# No delimiter found — the entire body is display-only (eval: false).
# This is a valid use case (equivalent to just eval: false).
pass

return "\n".join(out)


def process_qmd_file(path: Path) -> bool:
"""Process a single `.qmd` file, rewriting mock cells in place.

Parameters
----------
path
Path to the `.qmd` file.

Returns
-------
bool
`True` if the file was modified, `False` otherwise.
"""
content = path.read_text(encoding="utf-8")
if "#| source-code: mock" not in content:
return False

rewritten = expand_mock_cells(content)
if rewritten == content:
return False

path.write_text(rewritten, encoding="utf-8")
return True


def process_directory(directory: Path) -> list[str]:
"""Process all `.qmd` files under *directory*.

Parameters
----------
directory
Root directory to scan recursively.

Returns
-------
list[str]
Relative paths of files that were modified.
"""
modified: list[str] = []
for qmd in sorted(directory.rglob("*.qmd")):
if process_qmd_file(qmd):
try:
rel = str(qmd.relative_to(directory))
except ValueError:
rel = str(qmd)
modified.append(rel)
return modified
7 changes: 7 additions & 0 deletions great_docs/assets/_extensions/output-title/_extension.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
title: Output Title
author: Great Docs
version: 1.0.0
quarto-required: ">=1.3.0"
contributes:
filters:
- output-title.lua
101 changes: 101 additions & 0 deletions great_docs/assets/_extensions/output-title/output-title.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
-- output-title.lua — Quarto filter for labelling and framing code cell outputs
--
-- Usage in .qmd files:
--
-- ```{python}
-- #| output-title: "Response"
-- chat.chat("Hello!")
-- ```
--
-- ```{python}
-- #| output-frame: true
-- print("framed output, no title")
-- ```
--
-- Wraps the cell's output in a styled container with an optional title.
-- Works with any executable code cell, and composes with source-code: mock.

--- Extract the output-title value from a cell Div's attributes.
--- Quarto passes unrecognised hash-pipe options as attributes on the
--- enclosing div.cell element.
---@param div pandoc.Div
---@return string|nil The title text (unquoted), or nil if absent.
local function get_output_title(div)
-- Quarto passes custom cell options as attributes on the div
local title = div.attributes["output-title"]
if title == nil then return nil end

-- Strip surrounding quotes if present
title = title:match('^"(.*)"$') or title:match("^'(.*)'$") or title
if title == "" then return nil end
return title
end

--- Check whether output-frame is set to a truthy value.
---@param div pandoc.Div
---@return boolean
local function get_output_frame(div)
local val = div.attributes["output-frame"]
if val == nil then return false end
val = val:lower()
return val == "true" or val == "yes" or val == "1"
end

--- Check whether a block element is a cell-output container.
---@param el pandoc.Block
---@return boolean
local function is_cell_output(el)
if el.t ~= "Div" then return false end
for _, cls in ipairs(el.classes) do
if cls:match("^cell%-output") then return true end
end
return false
end

function Div(div)
-- Only operate on executable code cells
if not div.classes:includes("cell") then return nil end

local title = get_output_title(div)
local frame = get_output_frame(div)

-- Need either a title or an explicit frame request
if title == nil and not frame then return nil end

-- Remove attributes so they don't leak into the HTML
div.attributes["output-title"] = nil
div.attributes["output-frame"] = nil

-- Collect output blocks and wrap them
local new_content = pandoc.List()
local output_blocks = pandoc.List()

for _, el in ipairs(div.content) do
if is_cell_output(el) then
output_blocks:insert(el)
else
new_content:insert(el)
end
end

if #output_blocks == 0 then return nil end

-- Build the container HTML
local title_html = '<div class="gd-output-title-container">\n'
if title then
title_html = title_html
.. '<div class="gd-output-title-header">' .. title .. '</div>\n'
end
title_html = title_html .. '<div class="gd-output-title-body">\n'
local close_html = '</div>\n</div>'

-- Wrap: open tag, output blocks, close tag
new_content:insert(pandoc.RawBlock("html", title_html))
for _, ob in ipairs(output_blocks) do
new_content:insert(ob)
end
new_content:insert(pandoc.RawBlock("html", close_html))

div.content = new_content
return div
end
71 changes: 71 additions & 0 deletions great_docs/assets/great-docs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7809,6 +7809,77 @@ html[data-bs-theme="dark"] {
}


/* ── Output Title Container ─────────────────────────────────────────
Wraps code cell output with a labelled header.
Used by #| output-title: "..." hash-pipe option.
*/
.gd-output-title-container {
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0.375rem;
margin-top: 0.5rem;
margin-bottom: 1rem;
overflow: hidden;
}

.gd-output-title-header {
background: rgba(0, 0, 0, 0.03);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 0.35rem 0.75rem;
font-size: 0.8rem;
font-weight: 600;
color: rgba(0, 0, 0, 0.55);
letter-spacing: 0.02em;
}

.gd-output-title-body {
padding: 0.5rem 0.75rem;
}

.gd-output-title-body .cell-output {
margin: 0;
padding: 0;
}

.gd-output-title-body .cell-output pre {
margin: 0;
border: none;
background: transparent;
padding: 0;
}

/* Frameless variant for rich HTML outputs (GT tables, styled DataFrames, etc.)
These objects carry their own visual structure; a surrounding frame creates
an ugly double-border. We keep the title label but drop the container
border and body padding so the HTML output breathes.
We target display outputs that contain a child div (rich HTML wrapper)
rather than just a <pre> block (which is a plain return value). */
.gd-output-title-container:has(.gd-output-title-body .cell-output-display > div) {
border: none;
}

.gd-output-title-container:has(.gd-output-title-body .cell-output-display > div)
.gd-output-title-header {
background: transparent;
border-bottom: none;
padding-left: 0;
}

.gd-output-title-container:has(.gd-output-title-body .cell-output-display > div)
.gd-output-title-body {
padding: 0;
}

body.quarto-dark .gd-output-title-container {
border-color: rgba(255, 255, 255, 0.12);
}

body.quarto-dark .gd-output-title-header {
background: rgba(255, 255, 255, 0.04);
border-bottom-color: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.55);
}


/*-- scss:functions --*/

/*-- scss:uses --*/
Loading
Loading