From b4b3640a2513aea67d42bc53f7962c0505e8c9ab Mon Sep 17 00:00:00 2001 From: MykolaGolubyev Date: Sun, 19 Oct 2025 09:28:18 -0400 Subject: [PATCH 1/5] jupyter: improved output render and search --- .../add-2025-10-19-jupyter-improved-render.md | 2 + .../znai/jupyter/JupyterCell.java | 4 + .../znai/jupyter/JupyterIncludePlugin.java | 70 +++-- znai-reactjs/src/App.css | 1 + znai-reactjs/src/App.jsx | 2 + .../doc-elements/code-snippets/Snippet.jsx | 297 +++++++++--------- .../code-snippets/SnippetContainer.css | 4 + .../code-snippets/SnippetContainer.jsx | 268 ++++++++-------- .../SnippetResultOutput.demo.jsx | 47 +++ .../src/doc-elements/jupyter/Jupyter.demo.jsx | 15 +- .../doc-elements/jupyter/JupyterCodeCell.css | 3 +- .../doc-elements/jupyter/JupyterCodeCell.jsx | 24 +- .../doc-elements/jupyter/JupyterHtmlCell.css | 4 +- .../doc-elements/jupyter/JupyterHtmlCell.jsx | 22 +- .../doc-elements/jupyter/JupyterTextCell.jsx | 19 +- .../src/theme/znai-dark/znai-dark.css | 1 + 16 files changed, 437 insertions(+), 346 deletions(-) create mode 100644 znai-docs/znai/release-notes/1.81/add-2025-10-19-jupyter-improved-render.md create mode 100644 znai-reactjs/src/doc-elements/code-snippets/SnippetResultOutput.demo.jsx diff --git a/znai-docs/znai/release-notes/1.81/add-2025-10-19-jupyter-improved-render.md b/znai-docs/znai/release-notes/1.81/add-2025-10-19-jupyter-improved-render.md new file mode 100644 index 000000000..05de88d0d --- /dev/null +++ b/znai-docs/znai/release-notes/1.81/add-2025-10-19-jupyter-improved-render.md @@ -0,0 +1,2 @@ +* Add: Jupyter notebooks improved search and llm.txt support +* Add: Jupyter notebooks improved std output and tables cell rendering \ No newline at end of file diff --git a/znai-jupyter/src/main/java/org/testingisdocumenting/znai/jupyter/JupyterCell.java b/znai-jupyter/src/main/java/org/testingisdocumenting/znai/jupyter/JupyterCell.java index 23dc212f8..1c0fc8c6a 100644 --- a/znai-jupyter/src/main/java/org/testingisdocumenting/znai/jupyter/JupyterCell.java +++ b/znai-jupyter/src/main/java/org/testingisdocumenting/znai/jupyter/JupyterCell.java @@ -43,4 +43,8 @@ public String getInput() { public List getOutputs() { return outputs; } + + public boolean hasTextOutput() { + return outputs.stream().anyMatch((output) -> output.format().equals(JupyterOutput.TEXT_FORMAT)); + } } diff --git a/znai-jupyter/src/main/java/org/testingisdocumenting/znai/jupyter/JupyterIncludePlugin.java b/znai-jupyter/src/main/java/org/testingisdocumenting/znai/jupyter/JupyterIncludePlugin.java index 1b79805f4..07e64abde 100644 --- a/znai-jupyter/src/main/java/org/testingisdocumenting/znai/jupyter/JupyterIncludePlugin.java +++ b/znai-jupyter/src/main/java/org/testingisdocumenting/znai/jupyter/JupyterIncludePlugin.java @@ -17,13 +17,9 @@ package org.testingisdocumenting.znai.jupyter; -import org.testingisdocumenting.znai.codesnippets.CodeSnippetsProps; import org.testingisdocumenting.znai.core.AuxiliaryFile; import org.testingisdocumenting.znai.core.ComponentsRegistry; -import org.testingisdocumenting.znai.extensions.PluginParamType; -import org.testingisdocumenting.znai.extensions.PluginParams; -import org.testingisdocumenting.znai.extensions.PluginParamsDefinition; -import org.testingisdocumenting.znai.extensions.PluginResult; +import org.testingisdocumenting.znai.extensions.*; import org.testingisdocumenting.znai.extensions.include.IncludePlugin; import org.testingisdocumenting.znai.parser.ParserHandler; import org.testingisdocumenting.znai.parser.commonmark.MarkdownParser; @@ -41,9 +37,9 @@ public class JupyterIncludePlugin implements IncludePlugin { private static final String EXCLUDE_SECTION_TITLE_KEY = "excludeSectionTitle"; private MarkdownParser markdownParser; private Path path; - private String lang; private boolean isStoryFirst; private ParserHandler markdownParserHandler; + private PluginParamsFactory pluginParamsFactory; @Override public String id() { @@ -69,6 +65,7 @@ public PluginParamsDefinition parameters() { public PluginResult process(ComponentsRegistry componentsRegistry, ParserHandler parserHandler, Path markupPath, PluginParams pluginParams) { markdownParser = componentsRegistry.markdownParser(); markdownParserHandler = parserHandler; + pluginParamsFactory = componentsRegistry.pluginParamsFactory(); isStoryFirst = pluginParams.getOpts().get(STORY_FIRST_KEY, false); List includeSection = pluginParams.getOpts().getList(INCLUDE_SECTION_KEY); @@ -79,7 +76,6 @@ public PluginResult process(ComponentsRegistry componentsRegistry, ParserHandler JupyterNotebook notebook = new JupyterParserVer4() .parse(JsonUtils.deserializeAsMap(resourcesResolver.textContent(path))); - lang = notebook.getLang(); List cells = !includeSection.isEmpty() ? collectCells(notebook.getCells(), includeSection, excludeSectionTitle) : @@ -109,6 +105,7 @@ private List collectCells(List cells, List inc @Override public List textForSearch() { + // TODO extract output from html output cells as those are not being processed by markdown processor return List.of(); } @@ -137,15 +134,17 @@ private void processInputFromCell(JupyterCell cell) { return; } - Map props = new LinkedHashMap<>(); - props.put("cellType", cell.getType()); - props.putAll(convertInputData(cell)); - - if (isStoryFirst) { - props.putAll(createMetaRight()); + Map props = isStoryFirst ? createRightSideProp() : new HashMap<>(); + addJupyterCellClassName(props); + if (!cell.getOutputs().isEmpty()) { + props.put("noGap", true); + if (cell.hasTextOutput()) { + props.put("noGapBorder", true); + } } - addCell(props); + markdownParserHandler.onSnippet(pluginParamsFactory.create("snippet", "", props), + "python", "", cell.getInput()); } private void processOutputFromCell(JupyterCell cell) { @@ -157,15 +156,25 @@ private void processOutputFromCell(JupyterCell cell) { } private void processCellOutput(JupyterOutput output) { - Map props = new LinkedHashMap<>(); - props.put("cellType", "output"); - props.putAll(convertOutputData(output)); + if (output.format().equals(JupyterOutput.TEXT_FORMAT)) { + Map props = !isStoryFirst ? createRightSideProp() : new HashMap<>(); + props.put("resultOutput", true); + addJupyterCellClassName(props); + + markdownParserHandler. + onSnippet(pluginParamsFactory.create("snippet", "", props), + "csv", "", output.content()); + } else { + Map props = new LinkedHashMap<>(); + props.put("cellType", "output"); + props.putAll(convertOutputData(output)); - if (!isStoryFirst) { - props.putAll(createMetaRight()); - } + if (!isStoryFirst) { + props.putAll(createMetaRight()); + } - addCell(props); + addCell(props); + } } private void processEmptyOutput() { @@ -184,21 +193,22 @@ private boolean isMarkdown(JupyterCell cell) { return cell.getType().equals(JupyterCell.MARKDOWN_TYPE); } - private Map createMetaRight() { - Map meta = new LinkedHashMap<>(); - meta.put("rightSide", true); - + private Map createMetaRight() { + Map meta = createRightSideProp(); return Collections.singletonMap("meta", meta); } - private Map convertInputData(JupyterCell cell) { - if (cell.getType().equals(JupyterCell.CODE_TYPE)) { - return CodeSnippetsProps.create(lang, cell.getInput()); - } - return Collections.singletonMap(JupyterOutput.TEXT_FORMAT, cell.getInput()); + private static Map createRightSideProp() { + Map meta = new LinkedHashMap<>(); + meta.put("rightSide", true); + return meta; } private Map convertOutputData(JupyterOutput output) { return Collections.singletonMap(output.format(), output.content()); } + + private void addJupyterCellClassName(Map props) { + props.put("className", "znai-jupyter-cell"); + } } diff --git a/znai-reactjs/src/App.css b/znai-reactjs/src/App.css index 4ca5345e2..85662364a 100644 --- a/znai-reactjs/src/App.css +++ b/znai-reactjs/src/App.css @@ -156,6 +156,7 @@ --znai-landing-input-placeholder-color: #bbb; --znai-snippets-background-color: #f8fafc; + --znai-snippets-result-output-background-color: #f3f3f3; /* e.g., for combining example and output with noGap */ --znai-snippets-outer-border-color: #ddd; --znai-snippets-inner-border-color: #f3f3f3; diff --git a/znai-reactjs/src/App.jsx b/znai-reactjs/src/App.jsx index f3a069cc8..6359e6654 100644 --- a/znai-reactjs/src/App.jsx +++ b/znai-reactjs/src/App.jsx @@ -106,6 +106,7 @@ import { footnoteDemo } from "./doc-elements/footnote/Footnote.demo"; import { asciinemaDemo } from "./doc-elements/asciinema/Asciinema.demo"; import { previewConsoleOutputDemo } from "./screens/preview-change-path/PreviewConsoleOutput.demo"; import { readMoreDemo } from "./doc-elements/read-more/ReadMore.demo.js"; +import { snippetsResultOutputDemo } from "./doc-elements/code-snippets/SnippetResultOutput.demo.jsx"; const docMeta = { id: "preview", @@ -146,6 +147,7 @@ registries .add("snippets") .registerAsRows("containers title", containerTitleDemo) .registerAsGrid("Code Snippet", 0, snippetsDemo) + .registerAsGrid("Code Snippet Result Output", 0, snippetsResultOutputDemo) .registerAsGrid("Code Snippet With Bullets", 0, snippetsWithInlineCommentsDemo) .registerAsGrid("Code Snippet Removed Comments", 0, codeSnippetWithRemovedCommentsDemo) .registerAsGrid("Code Snippet Syntax Highlight ", 0, syntaxHighlightSnippetDemo) diff --git a/znai-reactjs/src/doc-elements/code-snippets/Snippet.jsx b/znai-reactjs/src/doc-elements/code-snippets/Snippet.jsx index dc80039cf..439e20b33 100644 --- a/znai-reactjs/src/doc-elements/code-snippets/Snippet.jsx +++ b/znai-reactjs/src/doc-elements/code-snippets/Snippet.jsx @@ -15,194 +15,197 @@ * limitations under the License. */ -import * as React from "react" +import * as React from "react"; import { - collapseCommentsAboveToMakeCommentOnTheCodeLine, - isCommentToken, removeCommentsFromEachLine, - splitTokensIntoLines, trimComment + collapseCommentsAboveToMakeCommentOnTheCodeLine, + isCommentToken, + removeCommentsFromEachLine, + splitTokensIntoLines, + trimComment } from "./codeUtils"; -import {isAllAtOnce} from '../meta/meta' -import {convertToList} from '../propsUtils'; +import { isAllAtOnce } from "../meta/meta"; +import { convertToList } from "../propsUtils"; -import SnippetContainer from './SnippetContainer' -import CodeSnippetWithCallouts from './CodeSnippetWithCallouts' -import SimpleCodeSnippet from './SimpleCodeSnippet' +import SnippetContainer from "./SnippetContainer"; +import CodeSnippetWithCallouts from "./CodeSnippetWithCallouts"; +import SimpleCodeSnippet from "./SimpleCodeSnippet"; -import {parseCode} from './codeParser' -import {countNumberOfLines} from "../../utils/strings"; +import { parseCode } from "./codeParser"; +import { countNumberOfLines } from "../../utils/strings"; -import { SnippetBulletExplanations } from './explanations/SnippetBulletExplanations'; +import { SnippetBulletExplanations } from "./explanations/SnippetBulletExplanations"; -import './Snippet.css' +import "./Snippet.css"; -const defaultNumberOfVisibleLines = 25 +const defaultNumberOfVisibleLines = 25; -const BULLETS_COMMENT_TYPE = 'inline' -const REMOVE_COMMENT_TYPE = 'remove' +const BULLETS_COMMENT_TYPE = "inline"; +const REMOVE_COMMENT_TYPE = "remove"; const Snippet = (props) => { - const tokensToUse = parseCodeWithCompatibility({lang: props.lang, snippet: props.snippet, tokens: props.tokens}) + const tokensToUse = parseCodeWithCompatibility({ lang: props.lang, snippet: props.snippet, tokens: props.tokens }); + + const renderBulletComments = + props.commentsType === BULLETS_COMMENT_TYPE || (props.callouts && Object.keys(props.callouts).length > 0); + + const snippetComponent = renderBulletComments ? CodeSnippetWithCallouts : SimpleCodeSnippet; + + const lines = splitTokensIntoLines(tokensToUse); + + const modifiedLines = mergeOrRemoveComments(); + const comments = renderBulletComments ? buildCalloutsFromComments(modifiedLines) : {}; + const mergedCallouts = { ...comments, ...props.callouts }; + + return ( + <> + + + + ); + + function mergeOrRemoveComments() { + if (renderBulletComments) { + return collapseCommentsAboveToMakeCommentOnTheCodeLine(lines); + } - const renderBulletComments = props.commentsType === BULLETS_COMMENT_TYPE || (props.callouts && Object.keys(props.callouts).length > 0); + if (props.commentsType === REMOVE_COMMENT_TYPE) { + return removeCommentsFromEachLine(lines); + } - const snippetComponent = renderBulletComments ? - CodeSnippetWithCallouts : - SimpleCodeSnippet + return lines; + } +}; - const lines = splitTokensIntoLines(tokensToUse); +Snippet.defaultProps = { + numberOfVisibleLines: defaultNumberOfVisibleLines +}; - const modifiedLines = mergeOrRemoveComments() - const comments = renderBulletComments ? buildCalloutsFromComments(modifiedLines) : {}; - const mergedCallouts = {...comments, ...props.callouts} +function Explanations({ spoiler, isPresentation, callouts = {}, elementsLibrary }) { + if (isPresentation || Object.keys(callouts).length === 0) { + return null; + } - return ( - <> - - - - ) - - function mergeOrRemoveComments() { - if (renderBulletComments) { - return collapseCommentsAboveToMakeCommentOnTheCodeLine(lines) - } - - if (props.commentsType === REMOVE_COMMENT_TYPE) { - return removeCommentsFromEachLine(lines) - } - - return lines - } + return ; } -Snippet.defaultProps = { - numberOfVisibleLines: defaultNumberOfVisibleLines +function scrollToLineIdx({ isPresentation, slideIdx, numberOfVisibleLines }) { + if (!isPresentation || !numberOfVisibleLines) { + return undefined; + } + + return numberOfVisibleLines * slideIdx; } -function Explanations({spoiler, isPresentation, callouts = {}, elementsLibrary}) { - if (isPresentation || Object.keys(callouts).length === 0) { - return null +const presentationSnippetHandler = { + component: Snippet, + numberOfSlides: ({ + meta, + commentsType, + lang, + snippet, + tokens, + highlight, + revealLineStop, + numberOfVisibleLines = defaultNumberOfVisibleLines + }) => { + const tokensToUse = parseCodeWithCompatibility({ lang, snippet, tokens }); + const highlightAsList = convertToList(highlight); + + if (commentsType === BULLETS_COMMENT_TYPE) { + return inlinedCommentsNumberOfSlides({ meta, tokens: tokensToUse }); } - return -} + const numberOfStopLines = (revealLineStop || []).length; + const numberOfScrolls = countNumberOfScrolls(); + + const hasFirstNoActionSlide = + highlightAsList.length > 0 || + numberOfStopLines > 0 || + (highlightAsList.length === 0 && numberOfStopLines === 0 && numberOfScrolls === 0); + + return ( + (hasFirstNoActionSlide ? 1 : 0) + + highlightNumberOfSlides({ meta, highlightAsList }) + + numberOfStopLines + + numberOfScrolls + ); + + function countNumberOfScrolls() { + const numberOfLines = countNumberOfLines(snippet); + if (numberOfLines <= numberOfVisibleLines) { + return 0; + } -function scrollToLineIdx({isPresentation, slideIdx, numberOfVisibleLines}) { - if (!isPresentation || !numberOfVisibleLines) { - return undefined + return Math.ceil(numberOfLines / numberOfVisibleLines); } + }, + slideInfoProvider: ({ meta, commentsType, lang, snippet, tokens, slideIdx }) => { + const tokensToUse = parseCodeWithCompatibility({ lang, snippet, tokens }); - return numberOfVisibleLines * slideIdx -} + if (isAllAtOnce(meta)) { + return {}; + } -const presentationSnippetHandler = { - component: Snippet, - numberOfSlides: ({ - meta, - commentsType, - lang, - snippet, - tokens, - highlight, - revealLineStop, - numberOfVisibleLines = defaultNumberOfVisibleLines - }) => { - const tokensToUse = parseCodeWithCompatibility({lang, snippet, tokens}) - const highlightAsList = convertToList(highlight) - - if (commentsType === BULLETS_COMMENT_TYPE) { - return inlinedCommentsNumberOfSlides({meta, tokens: tokensToUse}) - } - - const numberOfStopLines = (revealLineStop || []).length - const numberOfScrolls = countNumberOfScrolls() - - const hasFirstNoActionSlide = highlightAsList.length > 0 || numberOfStopLines > 0 || - (highlightAsList.length === 0 && numberOfStopLines === 0 && numberOfScrolls === 0) - - return (hasFirstNoActionSlide ? 1 : 0) + - highlightNumberOfSlides({meta, highlightAsList}) + - numberOfStopLines + - numberOfScrolls - - function countNumberOfScrolls() { - const numberOfLines = countNumberOfLines(snippet) - - if (numberOfLines <= numberOfVisibleLines) { - return 0 - } - - return Math.ceil(numberOfLines / numberOfVisibleLines) - } - }, - slideInfoProvider: ({meta, commentsType, lang, snippet, tokens, slideIdx}) => { - const tokensToUse = parseCodeWithCompatibility({lang, snippet, tokens}) - - if (isAllAtOnce(meta)) { - return {} - } - - if (commentsType !== BULLETS_COMMENT_TYPE) { - return {} - } - - const comments = tokensToUse.filter(t => isCommentToken(t)) - - return { - slideVisibleNote: !comments.length ? null : - slideIdx === 0 ? "" : comments[slideIdx - 1].content - } + if (commentsType !== BULLETS_COMMENT_TYPE) { + return {}; } -} + + const comments = tokensToUse.filter((t) => isCommentToken(t)); + + return { + slideVisibleNote: !comments.length ? null : slideIdx === 0 ? "" : comments[slideIdx - 1].content + }; + }, +}; // TODO for backward compatibility with already built and deployed docs // remove once TSI rebuilds all the docs -function parseCodeWithCompatibility({lang, tokens, snippet}) { - if (tokens) { - return tokens - } +function parseCodeWithCompatibility({ lang, tokens, snippet }) { + if (tokens) { + return tokens; + } - return parseCode(lang, snippet) + return parseCode(lang, snippet); } -function inlinedCommentsNumberOfSlides({meta, tokens}) { - const comments = tokens.filter(t => isCommentToken(t)) +function inlinedCommentsNumberOfSlides({ meta, tokens }) { + const comments = tokens.filter((t) => isCommentToken(t)); - if (isAllAtOnce(meta) && comments.length > 0) { - return 2 // two slides: 1st - no highlights; 2nd - all highlighted at once - } + if (isAllAtOnce(meta) && comments.length > 0) { + return 2; // two slides: 1st - no highlights; 2nd - all highlighted at once + } - return comments.length + 1 + return comments.length + 1; } -function highlightNumberOfSlides({meta, highlightAsList}) { - if (isAllAtOnce(meta) && highlightAsList.length > 0) { - return 1 - } +function highlightNumberOfSlides({ meta, highlightAsList }) { + if (isAllAtOnce(meta) && highlightAsList.length > 0) { + return 1; + } - return highlightAsList.length + return highlightAsList.length; } function buildCalloutsFromComments(lines) { - const result = {} - lines.forEach((line, lineIdx) => { - line.forEach(token => { - if (isCommentToken(token)) { - result[lineIdx] = [{type: "SimpleText", text: trimComment(token.content)}] - } - }) - }) - - return result + const result = {}; + lines.forEach((line, lineIdx) => { + line.forEach((token) => { + if (isCommentToken(token)) { + result[lineIdx] = [{ type: "SimpleText", text: trimComment(token.content) }]; + } + }); + }); + + return result; } -export {Snippet, presentationSnippetHandler} +export { Snippet, presentationSnippetHandler }; diff --git a/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.css b/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.css index 607f0ce06..139f003cd 100644 --- a/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.css +++ b/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.css @@ -20,6 +20,10 @@ background: var(--znai-snippets-background-color); } +.snippet-container.result-output { + background: var(--znai-snippets-result-output-background-color); +} + .snippet-container.no-margin-bottom { margin-bottom: 0; } diff --git a/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.jsx b/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.jsx index 8623d7829..e4e6a31dc 100644 --- a/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.jsx +++ b/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.jsx @@ -30,175 +30,171 @@ import { ContainerTitle } from "../container/ContainerTitle"; import "./SnippetContainer.css"; class SnippetContainer extends React.Component { - constructor(props) { - super(props); - this.state = { - displayCopied: false, - collapsed: this.props.collapsed - } - } + constructor(props) { + super(props); + this.state = { + displayCopied: false, + collapsed: this.props.collapsed + }; + } + + render() { + const { wide, isPresentation } = this.props; + const renderWide = wide && !isPresentation; + + return renderWide ? this.renderWideMode() : this.renderNormalMode(); + } + + renderNormalMode() { + const { title, className, resultOutput, noGap, noGapBorder, next, prev } = this.props; + + const fullClassName = + "snippet-container content-block" + (resultOutput ? " result-output" : "") + (className ? " " + className : ""); + + return ( + + {this.renderTitle(title)} + {this.renderSnippet()} + + ); + } - render() { - const {wide, isPresentation} = this.props - const renderWide = wide && !isPresentation + renderWideMode() { + const { title, className } = this.props; + + const wideModePadding =
; + + const fullClassName = + "snippet-container wide-screen" + (title ? " with-title" : "") + (className ? " " + className : ""); - return renderWide ? - this.renderWideMode() : this.renderNormalMode() - } + return ( +
+ {wideModePadding} + {title &&
{this.renderTitle(title)}
} - renderNormalMode() { - const {title, className, noGap, noGapBorder, next, prev} = this.props + {wideModePadding} - const fullClassName = "snippet-container content-block" - + (className ? " " + className : "") + {this.renderSnippet()} +
+ ); + } - return ( - - {this.renderTitle(title)} - {this.renderSnippet()} - - ) + renderTitle(title) { + if (!title) { + return null; } - renderWideMode() { - const {title, className} = this.props - - const wideModePadding =
- - const fullClassName = "snippet-container wide-screen" + - (title ? " with-title" : "") + - (className ? " " + className : "") - - return ( -
- {wideModePadding} - {title &&
- {this.renderTitle(title)} -
} + const anchorId = this.props.anchorId; - {wideModePadding} + const { collapsed } = this.state; - {this.renderSnippet()} -
- ) - } + return ( + + ); + } - renderTitle(title) { - if (!title) { - return null; - } + collapseToggle = () => { + this.setState((prev) => ({ collapsed: !prev.collapsed })); + }; - const anchorId = this.props.anchorId + renderSnippet() { + return ( +
+ + {this.renderCopyToClipboard()} +
+ ); + } - const { collapsed } = this.state; + renderCopyToClipboard() { + const { isPresentation } = this.props; + const { displayCopied } = this.state; - return ( - - ) + if (isPresentation) { + return null; } - collapseToggle = () => { - this.setState(prev => ({collapsed: !prev.collapsed})) - } + const className = "snippet-copy-to-clipboard " + (displayCopied ? "copied" : "copy"); - renderSnippet() { - return ( -
- - {this.renderCopyToClipboard()} -
- ); - } + return ( +
+ +
+ ); + } - renderCopyToClipboard() { - const {isPresentation} = this.props - const {displayCopied} = this.state + get snippetClassName() { + const { title } = this.props; + const { collapsed } = this.state; + return "snippet" + (title ? " with-title" : "") + (collapsed ? " collapsed" : ""); + } - if (isPresentation) { - return null - } + saveCopyToClipboardNode = (node) => { + this.copyToClipboardNode = node; + }; - const className = 'snippet-copy-to-clipboard ' + (displayCopied ? 'copied': 'copy') + componentDidMount() { + this.setupClipboard(); + } - return ( -
- -
- ) + componentDidUpdate(prevProps, prevState, snapshot) { + if (prevProps.collapsed !== this.props.collapsed) { + this.setState({ collapsed: this.props.collapsed }); } + } - get snippetClassName() { - const {title} = this.props - const {collapsed} = this.state - return "snippet" - + (title ? " with-title" : "") - + (collapsed ? " collapsed" : "") - } + componentWillUnmount() { + this.clearTimer(); + this.destroyClipboard(); + } - saveCopyToClipboardNode = (node) => { - this.copyToClipboardNode = node + setupClipboard() { + if (!this.copyToClipboardNode) { + return; } - componentDidMount() { - this.setupClipboard() - } + this.clipboard = new ClipboardJS(this.copyToClipboardNode, { + text: () => { + const { linesOfCode, tokensForClipboardProvider } = this.props; + this.setState({ displayCopied: true }); + this.startRemoveFeedbackTimer(); - componentDidUpdate(prevProps, prevState, snapshot) { - if (prevProps.collapsed !== this.props.collapsed) { - this.setState({ collapsed: this.props.collapsed }) - } - } + return extractTextFromTokens(tokensToUse()); - componentWillUnmount() { - this.clearTimer() - this.destroyClipboard() - } + function tokensToUse() { + if (tokensForClipboardProvider) { + return tokensForClipboardProvider(); + } - setupClipboard() { - if (! this.copyToClipboardNode) { - return + return linesOfCode.reduce((acc, curr) => acc.concat(curr).concat("\n"), []); } + }, + }); + } - this.clipboard = new ClipboardJS(this.copyToClipboardNode, { - text: () => { - const {linesOfCode, tokensForClipboardProvider} = this.props - this.setState({displayCopied: true}) - this.startRemoveFeedbackTimer() - - return extractTextFromTokens(tokensToUse()) - - function tokensToUse() { - if (tokensForClipboardProvider) { - return tokensForClipboardProvider() - } - - return linesOfCode.reduce((acc, curr) => acc.concat(curr).concat("\n"), []) - } - } - }) - } - - destroyClipboard() { - if (this.clipboard) { - this.clipboard.destroy() - } + destroyClipboard() { + if (this.clipboard) { + this.clipboard.destroy(); } + } - startRemoveFeedbackTimer() { - this.removeFeedbackTimer = setTimeout(() => { - this.setState({displayCopied: false}) - }, 200) - } + startRemoveFeedbackTimer() { + this.removeFeedbackTimer = setTimeout(() => { + this.setState({ displayCopied: false }); + }, 200); + } - clearTimer() { - if (this.removeFeedbackTimer) { - clearTimeout(this.removeFeedbackTimer) - } + clearTimer() { + if (this.removeFeedbackTimer) { + clearTimeout(this.removeFeedbackTimer); } + } } -export default SnippetContainer +export default SnippetContainer; diff --git a/znai-reactjs/src/doc-elements/code-snippets/SnippetResultOutput.demo.jsx b/znai-reactjs/src/doc-elements/code-snippets/SnippetResultOutput.demo.jsx new file mode 100644 index 000000000..b48532367 --- /dev/null +++ b/znai-reactjs/src/doc-elements/code-snippets/SnippetResultOutput.demo.jsx @@ -0,0 +1,47 @@ +/* + * Copyright 2020 znai maintainers + * Copyright 2019 TWO SIGMA OPEN SOURCE, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from "react"; + +import { elementsLibrary } from "../DefaultElementsLibrary"; + +import { codeWithMethodCalls, contentParagraph } from "../demo-utils/contentGenerators"; + +export function snippetsResultOutputDemo(registry) { + registry.add("result output no gap", () => ( + + )); +} + +function compactContentSnippet(content, { noGap, noGapBorder, resultOutput } = {}) { + return { + type: "Snippet", + lang: "java", + noGap: noGap, + resultOutput, + noGapBorder, + snippet: content, + }; +} diff --git a/znai-reactjs/src/doc-elements/jupyter/Jupyter.demo.jsx b/znai-reactjs/src/doc-elements/jupyter/Jupyter.demo.jsx index 21ed31b75..aeca0c67a 100644 --- a/znai-reactjs/src/doc-elements/jupyter/Jupyter.demo.jsx +++ b/znai-reactjs/src/doc-elements/jupyter/Jupyter.demo.jsx @@ -76,5 +76,18 @@ export function jupyterDemo(registry) { registry .add("code cell", () => ) .add("output text cell", () => ) - .add("output htlml cell", () => ); + .add("output html cells", () => { + const content = [ + { + type: "JupyterCell", + noGap: true, + ...simpleNotebook.cells[0], + }, + { + type: "JupyterCell", + ...simpleNotebook.cells[2], + }, + ]; + return ; + }); } diff --git a/znai-reactjs/src/doc-elements/jupyter/JupyterCodeCell.css b/znai-reactjs/src/doc-elements/jupyter/JupyterCodeCell.css index 4b2597946..da91bc3a1 100644 --- a/znai-reactjs/src/doc-elements/jupyter/JupyterCodeCell.css +++ b/znai-reactjs/src/doc-elements/jupyter/JupyterCodeCell.css @@ -1,4 +1,5 @@ /* + * Copyright 2025 znai maintainers * Copyright 2019 TWO SIGMA OPEN SOURCE, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,7 +15,7 @@ * limitations under the License. */ -.page-two-sides-layout .jupyter-cell { +.page-two-sides-layout .znai-jupyter-cell { --znai-snippets-space-above: 10px; --znai-snippets-space-below: 10px; } diff --git a/znai-reactjs/src/doc-elements/jupyter/JupyterCodeCell.jsx b/znai-reactjs/src/doc-elements/jupyter/JupyterCodeCell.jsx index af0eb1c9d..50da02ebe 100644 --- a/znai-reactjs/src/doc-elements/jupyter/JupyterCodeCell.jsx +++ b/znai-reactjs/src/doc-elements/jupyter/JupyterCodeCell.jsx @@ -1,4 +1,5 @@ /* + * Copyright 2025 znai maintainers * Copyright 2019 TWO SIGMA OPEN SOURCE, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,16 +15,19 @@ * limitations under the License. */ -import React from 'react' +import React from "react"; -import './JupyterCodeCell.css' +import "./JupyterCodeCell.css"; -const JupyterCodeCell = ({snippet, lang, elementsLibrary}) => { - return ( -
- -
- ) -} +const JupyterCodeCell = ({ snippet, lang, elementsLibrary, ...props }) => { + return ( + + ); +}; -export default JupyterCodeCell +export default JupyterCodeCell; diff --git a/znai-reactjs/src/doc-elements/jupyter/JupyterHtmlCell.css b/znai-reactjs/src/doc-elements/jupyter/JupyterHtmlCell.css index fd054c475..d70e0c43b 100644 --- a/znai-reactjs/src/doc-elements/jupyter/JupyterHtmlCell.css +++ b/znai-reactjs/src/doc-elements/jupyter/JupyterHtmlCell.css @@ -21,6 +21,7 @@ } .jupyter-html table { + width: 100%; margin: 0; border-collapse: collapse; background-color: var(--znai-table-body-background-color); @@ -32,11 +33,10 @@ } .jupyter-html thead { - background-color: var(--znai-table-header-background-color); } .jupyter-html th { - background: var(--znai-table-header-background-color); + background: var(--znai-snippets-result-output-background-color); padding: 12px 16px; font-weight: 600; text-align: left; diff --git a/znai-reactjs/src/doc-elements/jupyter/JupyterHtmlCell.jsx b/znai-reactjs/src/doc-elements/jupyter/JupyterHtmlCell.jsx index af52ffaca..79787be39 100644 --- a/znai-reactjs/src/doc-elements/jupyter/JupyterHtmlCell.jsx +++ b/znai-reactjs/src/doc-elements/jupyter/JupyterHtmlCell.jsx @@ -1,4 +1,5 @@ /* + * Copyright 2025 znai maintainers * Copyright 2019 TWO SIGMA OPEN SOURCE, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,16 +15,17 @@ * limitations under the License. */ -import React from 'react' +import React from "react"; -import './JupyterHtmlCell.css' +import { Container } from "../container/Container.js"; +import "./JupyterHtmlCell.css"; -const JupyterHtmlCell = ({html, elementsLibrary}) => { - return ( -
-
-
- ) -} +const JupyterHtmlCell = ({ html, elementsLibrary, ...props }) => { + return ( + +
+ + ); +}; -export default JupyterHtmlCell +export default JupyterHtmlCell; diff --git a/znai-reactjs/src/doc-elements/jupyter/JupyterTextCell.jsx b/znai-reactjs/src/doc-elements/jupyter/JupyterTextCell.jsx index 6647576ef..1e5a8ef8a 100644 --- a/znai-reactjs/src/doc-elements/jupyter/JupyterTextCell.jsx +++ b/znai-reactjs/src/doc-elements/jupyter/JupyterTextCell.jsx @@ -1,4 +1,5 @@ /* + * Copyright 2025 znai maintainers * Copyright 2019 TWO SIGMA OPEN SOURCE, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,14 +15,14 @@ * limitations under the License. */ -import React from 'react' +import React from "react"; -const JupyterTextCell = ({text, elementsLibrary}) => { - return ( -
- -
- ) -} +const JupyterTextCell = ({ text, elementsLibrary }) => { + return ( +
+ +
+ ); +}; -export default JupyterTextCell +export default JupyterTextCell; diff --git a/znai-reactjs/src/theme/znai-dark/znai-dark.css b/znai-reactjs/src/theme/znai-dark/znai-dark.css index d652219e8..25b0ea634 100644 --- a/znai-reactjs/src/theme/znai-dark/znai-dark.css +++ b/znai-reactjs/src/theme/znai-dark/znai-dark.css @@ -113,6 +113,7 @@ --znai-landing-input-placeholder-color: #555; --znai-snippets-background-color: #181e21; + --znai-snippets-result-output-background-color: #131718; --znai-snippets-outer-border-color: #29343a; --znai-snippets-inner-border-color: #222c31; --znai-snippets-title-color: #959595; From f475b490668e03cd989b1c6a693c3bf4c246e6e0 Mon Sep 17 00:00:00 2001 From: MykolaGolubyev Date: Sun, 19 Oct 2025 09:30:59 -0400 Subject: [PATCH 2/5] jupyter: improved output render and search --- .../doc-elements/code-snippets/Snippet.jsx | 24 +++++++++---------- .../code-snippets/SnippetContainer.jsx | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/znai-reactjs/src/doc-elements/code-snippets/Snippet.jsx b/znai-reactjs/src/doc-elements/code-snippets/Snippet.jsx index 439e20b33..61a808049 100644 --- a/znai-reactjs/src/doc-elements/code-snippets/Snippet.jsx +++ b/znai-reactjs/src/doc-elements/code-snippets/Snippet.jsx @@ -22,7 +22,7 @@ import { isCommentToken, removeCommentsFromEachLine, splitTokensIntoLines, - trimComment + trimComment, } from "./codeUtils"; import { isAllAtOnce } from "../meta/meta"; import { convertToList } from "../propsUtils"; @@ -85,7 +85,7 @@ const Snippet = (props) => { }; Snippet.defaultProps = { - numberOfVisibleLines: defaultNumberOfVisibleLines + numberOfVisibleLines: defaultNumberOfVisibleLines, }; function Explanations({ spoiler, isPresentation, callouts = {}, elementsLibrary }) { @@ -107,15 +107,15 @@ function scrollToLineIdx({ isPresentation, slideIdx, numberOfVisibleLines }) { const presentationSnippetHandler = { component: Snippet, numberOfSlides: ({ - meta, - commentsType, - lang, - snippet, - tokens, - highlight, - revealLineStop, - numberOfVisibleLines = defaultNumberOfVisibleLines - }) => { + meta, + commentsType, + lang, + snippet, + tokens, + highlight, + revealLineStop, + numberOfVisibleLines = defaultNumberOfVisibleLines, + }) => { const tokensToUse = parseCodeWithCompatibility({ lang, snippet, tokens }); const highlightAsList = convertToList(highlight); @@ -162,7 +162,7 @@ const presentationSnippetHandler = { const comments = tokensToUse.filter((t) => isCommentToken(t)); return { - slideVisibleNote: !comments.length ? null : slideIdx === 0 ? "" : comments[slideIdx - 1].content + slideVisibleNote: !comments.length ? null : slideIdx === 0 ? "" : comments[slideIdx - 1].content, }; }, }; diff --git a/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.jsx b/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.jsx index e4e6a31dc..fdba8ff20 100644 --- a/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.jsx +++ b/znai-reactjs/src/doc-elements/code-snippets/SnippetContainer.jsx @@ -34,7 +34,7 @@ class SnippetContainer extends React.Component { super(props); this.state = { displayCopied: false, - collapsed: this.props.collapsed + collapsed: this.props.collapsed, }; } From b746896b168fb31c388bc5122568cc5b92a8a4f4 Mon Sep 17 00:00:00 2001 From: MykolaGolubyev Date: Sun, 19 Oct 2025 12:39:44 -0400 Subject: [PATCH 3/5] fix tests --- .../jupyter/JupyterIncludePluginTest.groovy | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/znai-jupyter/src/test/groovy/org/testingisdocumenting/znai/jupyter/JupyterIncludePluginTest.groovy b/znai-jupyter/src/test/groovy/org/testingisdocumenting/znai/jupyter/JupyterIncludePluginTest.groovy index 3a642d32d..f31c6dd5f 100644 --- a/znai-jupyter/src/test/groovy/org/testingisdocumenting/znai/jupyter/JupyterIncludePluginTest.groovy +++ b/znai-jupyter/src/test/groovy/org/testingisdocumenting/znai/jupyter/JupyterIncludePluginTest.groovy @@ -24,13 +24,13 @@ class JupyterIncludePluginTest { @Test void "should split each cell and create separate doc elements for each input and output"() { def elements = process("jupyter-notebook.ipynb") - elements.should == [[type: 'JupyterCell', cellType: 'code', snippet: 'from pandas import read_csv\nfrom IPython.display import display', lang: 'python'], + elements.should == [[type: 'Snippet', snippet: 'from pandas import read_csv\nfrom IPython.display import display', lang: 'python', className: "znai-jupyter-cell", lineNumber: ""], [type: 'JupyterCell', cellType: 'empty-output', meta: [rightSide: true]], - [type: 'JupyterCell', cellType: 'code', snippet: "tran = read_csv('transport.csv')\nprint(tran)", lang: 'python'], - [type: 'JupyterCell', cellType: 'output', text: ' a b c\n' + + [type: 'Snippet', snippet: "tran = read_csv('transport.csv')\nprint(tran)", lang: 'python', className: "znai-jupyter-cell", lineNumber: "", noGap: true, noGapBorder: true], + [type: 'Snippet', snippet: ' a b c\n' + '0 1 2 3\n' + - '1 4 5 6\n', meta: [rightSide: true]], - [type: 'JupyterCell', cellType: 'code', snippet: 'display(tran)', lang: 'python'], + '1 4 5 6', meta: [rightSide: true], className: "znai-jupyter-cell", lineNumber: "", lang: "csv", resultOutput: true], + [type: 'Snippet', snippet: 'display(tran)', lang: 'python', noGap: true, className: "znai-jupyter-cell", lineNumber: ""], [type: 'JupyterCell', cellType: 'output', meta: [rightSide: true], html:'\n' + ' \n' + ' \n' + @@ -54,19 +54,19 @@ class JupyterIncludePluginTest { 'JupyterCell' | 'empty-output' | true 'JupyterCell' | 'empty-output' | true - 'JupyterCell' | 'code' | true + 'Snippet' | '' | true 'TestMarkdown' | '' | false 'JupyterCell' | 'empty-output' | true - 'JupyterCell' | 'output' | false - 'JupyterCell' | 'code' | true + 'Snippet' | '' | false + 'Snippet' | '' | true 'TestMarkdown' | '' | false 'JupyterCell' | 'empty-output' | true 'JupyterCell' | 'output' | false - 'JupyterCell' | 'code' | true } + 'Snippet' | '' | true } } From 4df4a1902e251a598343c79100e201c6f5536685 Mon Sep 17 00:00:00 2001 From: MykolaGolubyev Date: Sun, 19 Oct 2025 12:47:45 -0400 Subject: [PATCH 4/5] update e2e test --- .../examples/webtauexamples/imageCapture.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/znai-testing-examples/examples/webtauexamples/imageCapture.groovy b/znai-testing-examples/examples/webtauexamples/imageCapture.groovy index 25e35a46b..6733126f8 100644 --- a/znai-testing-examples/examples/webtauexamples/imageCapture.groovy +++ b/znai-testing-examples/examples/webtauexamples/imageCapture.groovy @@ -2,7 +2,7 @@ package webtauexamples import static org.testingisdocumenting.webtau.WebTauGroovyDsl.* -def homeSearchInput = $('input[class*="searchbox_input"]') +def homeSearchInput = $('input[class*="searchbox_input"], input[class*="search-input"]') def resultSearchInput = $("#search_form_input") def result = $('article[data-testid="result"]') From 2a94b67db420bfc6af523d90314a2ab379dda5bb Mon Sep 17 00:00:00 2001 From: MykolaGolubyev Date: Sun, 19 Oct 2025 13:27:34 -0400 Subject: [PATCH 5/5] e2e test tweaks --- znai-tests/src/test/groovy/scenarios/presentation.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/znai-tests/src/test/groovy/scenarios/presentation.groovy b/znai-tests/src/test/groovy/scenarios/presentation.groovy index edb4121c6..884e634c7 100644 --- a/znai-tests/src/test/groovy/scenarios/presentation.groovy +++ b/znai-tests/src/test/groovy/scenarios/presentation.groovy @@ -30,6 +30,7 @@ scenario("open browser with docs") { } scenario("switch to presentation mode and validate title") { + standardView.presentationButton.waitToBe visible standardView.presentationButton.click() presentationContent.title.waitTo == "What Is This" }