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
97 changes: 97 additions & 0 deletions hugo/assets/css/directives-ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,103 @@
display: none;
}

/* ---------------------------------------------------------------------------
* tut-prereq-card — the ::requires[slug] prerequisite card
* Emitted by layouts/shortcodes/tut-requires.html. Without these rules the card
* collapses to 0x0 (the selectors existed in no stylesheet before this).
* ------------------------------------------------------------------------- */

.tut-prereq-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin: 1rem 0;
padding: 0.75rem 1rem;
border: 1px solid var(--sapList_BorderColor, #d9d9d9);
border-left: 4px solid var(--sapUiHighlight, #0070f2);
border-radius: 6px;
background: var(--sapList_Background, #fff);
}

.tut-prereq-card > a {
display: flex;
flex-direction: column;
gap: 0.15rem;
text-decoration: none;
color: inherit;
flex: 1 1 auto;
min-width: 0;
}

.tut-prereq-card > a:hover .prereq-title {
text-decoration: underline;
}

.tut-prereq-card .prereq-title {
font-weight: 600;
color: var(--sapLinkColor, #0070f2);
}

.tut-prereq-card .prereq-meta {
font-size: 0.85em;
color: var(--sapContent_LabelColor, #6a6d70);
}

.tut-prereq-card .prereq-status {
flex: 0 0 auto;
font-size: 0.85em;
color: var(--sapContent_LabelColor, #6a6d70);
}

.tut-prereq-card .prereq-status:empty {
display: none;
}

/* ---------------------------------------------------------------------------
* tut-tested-with — collapsible panel of tested tool versions, from the
* testedWith frontmatter map. Rendered inline by u1-object-page.html (the
* shortcode is never injected into the composed body).
* ------------------------------------------------------------------------- */

.tut-tested-with {
margin: 1rem 0;
}

.tut-tested-with details {
border: 1px solid var(--sapList_BorderColor, #d9d9d9);
border-radius: 6px;
background: var(--sapList_Background, #fff);
padding: 0.5rem 0.75rem;
}

.tut-tested-with summary {
cursor: pointer;
font-weight: 600;
color: var(--sapTextColor, #32363a);
}

.tut-tested-with ul {
margin: 0.5rem 0 0;
padding-left: 1.25rem;
list-style: none;
}

.tut-tested-with li {
display: flex;
gap: 0.5rem;
padding: 0.15rem 0;
}

.tut-tested-with .pkg {
font-family: var(--sapCode_FontFamily, monospace);
color: var(--sapTextColor, #32363a);
}

.tut-tested-with .ver {
color: var(--sapContent_LabelColor, #6a6d70);
}

/* ---------------------------------------------------------------------------
* Print: hide interactive / icon-only elements
* tut-menu and tut-kbd print as-is (their text content is meaningful in print)
Expand Down
5 changes: 5 additions & 0 deletions hugo/assets/css/sap-fundamental.src.css
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,11 @@ a:hover {
margin: 0;
border-radius: 0;
border: none;
/* Long single lines (e.g. a console command with a full URL) must not run off
the page. Default to horizontal scroll; the opt-in data-tut-code-wrap="on"
preference (ui5-overrides.css) switches this to wrapping. */
white-space: pre;
overflow-x: auto;
}

pre {
Expand Down
14 changes: 9 additions & 5 deletions hugo/assets/css/ui5-overrides.css
Original file line number Diff line number Diff line change
Expand Up @@ -777,16 +777,20 @@ html[data-tut-header="autohide"][data-page-kind="tutorial"] .op-header:focus-wit
}

/* Auto-hide footer: park it off the bottom edge; reveal on hover. Taskbar-style —
content is not padded for it; it overlays on reveal. */
html[data-tut-footer="autohide"][data-page-kind="tutorial"] footer {
content is not padded for it; it overlays on reveal.
Scoped to `body > footer.site-footer` (the page footer) — a bare `footer`
descendant selector also matched the Joule panel's nested
`<footer class="joule-panel__footer">`, forcing it position:fixed/left:0/right:0
so the chat input spanned the whole viewport instead of docking in the panel. */
html[data-tut-footer="autohide"][data-page-kind="tutorial"] body > footer.site-footer {
position: fixed;
left: 0; right: 0; bottom: 0;
transform: translateY(calc(100% - 8px)); /* 8px reveal strip keeps footer hover-reachable */
transition: transform 200ms ease;
z-index: 25;
}
html[data-tut-footer="autohide"][data-page-kind="tutorial"] footer:hover,
html[data-tut-footer="autohide"][data-page-kind="tutorial"] footer:focus-within {
html[data-tut-footer="autohide"][data-page-kind="tutorial"] body > footer.site-footer:hover,
html[data-tut-footer="autohide"][data-page-kind="tutorial"] body > footer.site-footer:focus-within {
transform: translateY(0);
}

Expand All @@ -796,7 +800,7 @@ html[data-tut-feedback="off"][data-page-kind="tutorial"] #op-discussion { displa

@media (prefers-reduced-motion: reduce) {
html[data-tut-header="autohide"][data-page-kind="tutorial"] .op-header,
html[data-tut-footer="autohide"][data-page-kind="tutorial"] footer { transition: none; }
html[data-tut-footer="autohide"][data-page-kind="tutorial"] body > footer.site-footer { transition: none; }
}

/* #1966 batch 2 — reading preferences. All rules tutorial-scoped. */
Expand Down
8 changes: 8 additions & 0 deletions hugo/hugo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ enableRobotsTXT = false # we ship our own via the robots output format
[markup.goldmark.renderer]
unsafe = true
[markup.goldmark.parser]
# #directive-hotspots: a standalone image with a following block-attribute
# line (`![](img)\n{hotspots=…}`) only has its attributes delivered to the
# image render hook's .Attributes when the image is NOT wrapped in a <p>.
# With the default (true) goldmark wraps it and the attribute block never
# attaches, so `{hotspots=…}` leaked as literal text and no overlay rendered.
# The render hooks already emit <figure>/<div> wrappers, so dropping the
# redundant <p> is also cleaner HTML. No CSS depends on p>img (checked).
wrapStandAloneImageWithinParagraph = false
[markup.goldmark.parser.attribute]
block = true
title = true
Expand Down
27 changes: 18 additions & 9 deletions hugo/layouts/_default/_markup/render-codeblock.html
Original file line number Diff line number Diff line change
Expand Up @@ -235,18 +235,27 @@
├── or └── connectors → stripped (depth comes from │ count);
lone ─ → stripped */}}
{{- $normalized := $line | replaceRE `│\s{0,3}` " " | replaceRE `[├└]─*\s*` "" | replaceRE `─` "" -}}
{{/* Count leading spaces for depth using strings.TrimLeft */}}
{{- $stripped := strings.TrimLeft $normalized " " -}}
{{/* State prefix (+ ~ -): the author writes the glyph in column 0, so a
state line indents one level LESS than a same-depth unprefixed line.
Replace a single leading glyph with ONE space so indentation (and thus
depth) is measured consistently — e.g. "+ srv/" → " srv/" (depth 1,
sibling of " db/"), "+ cat-service.cds" → " cat-service.cds"
(depth 2). Capture the glyph first for the state CSS class. */}}
{{- $state := "" -}}
{{- if findRE `^[+~-] ` $normalized -}}
{{- $state = substr $normalized 0 1 -}}
{{- $normalized = printf " %s" (substr $normalized 1 (len $normalized)) -}}
{{- end -}}
{{/* Count leading spaces for depth. NB: Hugo's strings.TrimLeft takes the
CUTSET first, then the string — `strings.TrimLeft " " $s` strips
leading spaces from $s. (The old code had the args reversed, which
trimmed the filename chars off a single space and produced garbage
depths + empty labels — the step-4 tree rendering bug.) */}}
{{- $stripped := strings.TrimLeft " " $normalized -}}
{{- $leading := sub (len $normalized) (len $stripped) -}}
{{/* depth = floor(leading / 2) — use integer division (truncating = floor for positive) */}}
{{/* depth = floor(leading / 2) — integer division (floor for positive) */}}
{{- $depth := div $leading 2 -}}
{{- $rest := trim $stripped " \t" -}}
{{/* Extract state prefix (+ ~ -) */}}
{{- $state := "" -}}
{{- if or (hasPrefix $rest "+ ") (hasPrefix $rest "~ ") (hasPrefix $rest "- ") -}}
{{- $state = substr $rest 0 1 -}}
{{- $rest = substr $rest 2 (len $rest) -}}
{{- end -}}
{{/* Extract annotation: two or more spaces + # */}}
{{- $annotation := "" -}}
{{- if findRE `\s{2,}# ` $rest -}}
Expand Down
27 changes: 26 additions & 1 deletion hugo/layouts/shortcodes/tut-walkthrough.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,33 @@
*/}}
{{- $code := .Get "code" | replaceRE "&#13;" "" | replaceRE "&#10;" "\n" -}}
{{- $notes := .Get "notes" | replaceRE "&#13;" "" | replaceRE "&#10;" "\n" -}}
{{/* Render the fenced code through Hugo's `highlight` function rather than
markdownify. code-walkthrough.ts highlights per-line by selecting
`.tut-walkthrough-code .line`, and Chroma's `highlight` output wraps every
line in `<span class="line">` (noClasses=false in hugo.toml) — whereas a
plain `markdownify` of the same fence emits NO per-line spans, so the
click-to-highlight found zero `.line` elements and highlighted nothing.
Strip the ``` fences and extract the language token from the first line. */}}
{{- $codeTrimmed := trim $code "\n" -}}
{{- $codeLines := split $codeTrimmed "\n" -}}
{{- $lang := "" -}}
{{- $body := $codeTrimmed -}}
{{- if and (gt (len $codeLines) 0) (hasPrefix (index $codeLines 0) "```") -}}
{{- $lang = trim (strings.TrimPrefix "```" (index $codeLines 0)) " \t" -}}
{{/* Drop the opening fence line and any trailing closing fence line. */}}
{{- $inner := after 1 $codeLines -}}
{{- if and (gt (len $inner) 0) (eq (trim (index $inner (sub (len $inner) 1)) " \t") "```") -}}
{{- $inner = first (sub (len $inner) 1) $inner -}}
{{- end -}}
{{- $body = delimit $inner "\n" -}}
{{- end -}}
{{- if eq $lang "" -}}{{- $lang = "text" -}}{{- end -}}
<div class="tut-walkthrough" data-walkthrough>
<div class="tut-walkthrough-code">{{ $code | markdownify }}</div>
{{/* security-reviewed: $body is the author's fenced code content; `highlight`
renders it through Chroma which HTML-escapes the source. $lang is the
fence's language token (alnum). Only Chroma's fixed line/token span
scaffold is emitted — no attacker-controlled HTML. */}}
<div class="tut-walkthrough-code">{{ highlight $body $lang "" }}</div>
<div class="tut-walkthrough-notes">{{ $notes | markdownify }}</div>
{{- with .Inner }}{{ . }}{{ end -}}
</div>
16 changes: 16 additions & 0 deletions hugo/layouts/tutorials/u1-object-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,22 @@ <h3>You will learn</h3>
<ul>{{ range . }}<li><span class="check-icon">✔</span><span class="check-text">{{ . | markdownify }}</span></li>{{ end }}</ul>
</div>
{{ end }}
{{/* "Tested with" panel — rendered from the testedWith frontmatter map.
The tut-tested-with shortcode is never injected into the composed
body (render-frontmatter.ts emits the param but no call), so the
layout renders it here directly. Mirrors shortcodes/tut-tested-with.html. */}}
{{ with .Params.testedWith }}
<div class="tut-tested-with">
<details>
<summary>Tested with</summary>
<ul>
{{- range $k, $v := . -}}
<li><span class="pkg">{{ $k }}</span> <span class="ver">{{ $v }}</span></li>
{{- end -}}
</ul>
</details>
</div>
{{ end }}
{{ partial "tutorial-author.html" . }}
{{ if not site.Params.previewMode }}{{ partial "more-from-author.html" . }}{{ end }}
</section>
Expand Down
36 changes: 36 additions & 0 deletions scripts/__tests__/images.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,40 @@ describe('resolveImageURLs', () => {
expect(result).toContain(`${base}/27.png`)
})
})

describe('moves inline image {attributes} to a block-attribute line (hotspots)', () => {
// goldmark only attaches {key=val} attribute lists to BLOCK elements, not to
// an inline image inside a paragraph. Authors write hotspots inline:
// ![alt](img.png){hotspots="1:22%,34% …"}
// which leaks as literal text (no overlay). Moving the {…} onto the next line
// under a standalone image makes goldmark attach it → render-image hook reads
// .Attributes.hotspots and emits the .tut-hotspot overlay anchors.
it('splits a standalone image + attr onto two lines', () => {
const input = '![Deployed apps](cockpit-space-apps.png){hotspots="1:22%,34% 2:55%,34%"}'
const result = resolveImageURLs(input, opts)
expect(result).toBe(
`![Deployed apps](${base}/cockpit-space-apps.png)\n{hotspots="1:22%,34% 2:55%,34%"}`
)
})

it('preserves leading indentation on both lines', () => {
const input = ' ![a](27.png){hotspots="1:10%,10%"}'
const result = resolveImageURLs(input, opts)
expect(result).toBe(` ![a](${base}/27.png)\n {hotspots="1:10%,10%"}`)
})

it('does NOT split an image that is inline within prose', () => {
// An attribute block cannot attach to an inline image anyway, and splitting
// mid-sentence would corrupt the paragraph.
const input = 'see ![a](27.png){x=1} here'
const result = resolveImageURLs(input, opts)
expect(result).toContain(`![a](${base}/27.png){x=1}`)
expect(result).not.toContain('\n{x=1}')
})

it('leaves a plain standalone image (no attrs) unchanged aside from URL', () => {
const input = '![a](27.png)'
expect(resolveImageURLs(input, opts)).toBe(`![a](${base}/27.png)`)
})
})
})
Loading
Loading