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
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
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)`)
})
})
})
25 changes: 25 additions & 0 deletions scripts/parsers/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,30 @@ export function resolveImageURLs(content: string, opts: ImageResolveOpts): strin
)
}

result = moveInlineImageAttributesToBlock(result)
return stripImageDirectiveComments(result)
}

/**
* Hugo's goldmark attribute parser (`markup.goldmark.parser.attribute.block=true`)
* attaches `{key=val}` attribute lists to BLOCK elements only — NOT to an inline
* image sitting inside a paragraph. Authors write image hotspots as an inline
* suffix: `![alt](img.png){hotspots="1:22%,34% …"}` — which goldmark leaves as
* literal text after the image (the leak observed on directive-rich-media), so
* the render-image hook never sees `hotspots` in `.Attributes` and no overlay
* dots render.
*
* Fix: when an image is ALONE on its line with a trailing `{…}` attribute block,
* move the `{…}` onto the FOLLOWING line. A standalone image is its own block,
* and a block-attribute line directly beneath it IS attached by goldmark → the
* render hook receives `.Attributes`. Images that are not alone on their line
* (inline in prose) are left untouched — an attribute block can't attach there
* anyway, and splitting would corrupt the sentence.
*/
export function moveInlineImageAttributesToBlock(content: string): string {
return content.replace(
// ^(indent)(![alt](path))( {attrs})$ — the whole line is image + attr block
/^([ \t]*)(!\[[^\]]*\]\([^)]+\))[ \t]*(\{[^}\n]*\})[ \t]*$/gm,
(_m, indent: string, img: string, attrs: string) => `${indent}${img}\n${indent}${attrs}`
)
}
Loading