Skip to content
Open
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
144 changes: 133 additions & 11 deletions components/blocks/code.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef } from "react";
import classNames from "classnames";
import Prism from "prismjs";
import "prismjs/plugins/line-numbers/prism-line-numbers";
Expand All @@ -7,8 +7,13 @@ import "prismjs/plugins/line-highlight/prism-line-highlight.css";
import "prismjs/plugins/toolbar/prism-toolbar";
import "prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard";
import "prismjs/plugins/normalize-whitespace/prism-normalize-whitespace";
import "prismjs/plugins/diff-highlight/prism-diff-highlight";
import "prismjs/plugins/diff-highlight/prism-diff-highlight.css";

import Image from "./image";
import languageDisplayNames, {
getPrismLanguage,
} from "../../lib/languageDisplayNames";

import styles from "./code.module.css";

Expand Down Expand Up @@ -68,10 +73,6 @@ const TryMeButton = ({ code }) => {
);
};

import languageDisplayNames, {
getPrismLanguage,
} from "../../lib/languageDisplayNames";

// Initialize the cache for imported languages.
const languageImports = new Map();

Expand Down Expand Up @@ -123,7 +124,10 @@ const Code = ({

// Extract language identifier for display
const langId = languageClass?.substring(9) || language || "python";
const displayLanguage = languageDisplayNames[langId] || langId;
const diffMatch = langId.match(/^diff-([\w-]+)$/);
const displayLanguage = diffMatch
? languageDisplayNames[diffMatch[1]] || diffMatch[1]
: languageDisplayNames[langId] || langId;
const showLanguage =
langId.toLowerCase() !== "none" && (showAll || !filename);

Expand Down Expand Up @@ -168,15 +172,131 @@ const Code = ({
);
};

// Strip deleted lines and diff prefixes (+/=) to produce copy-friendly text.
function getCleanDiffText(textContent) {
return textContent
.split(/\r?\n/)
.filter((line) => !line.startsWith("-"))
.map((line) => line.substring(1))
.join("\n");
}

// Add +/- markers into the left margin of the code block.
function addDiffMarkers(container, codeElement) {
const pre = codeElement.closest("pre");
if (!pre) return;

const lines = codeElement.textContent.split(/\r?\n/);
// Drop trailing empty line from a final newline
if (lines.length > 0 && lines[lines.length - 1] === "") {
lines.pop();
}

const markerEl = document.createElement("div");
markerEl.className = `diff-markers ${styles.DiffMarkers}`;
markerEl.setAttribute("aria-hidden", "true");

for (const line of lines) {
const span = document.createElement("span");
if (line.startsWith("+")) {
span.textContent = "+";
span.className = "diff-marker-insert";
} else if (line.startsWith("-")) {
span.textContent = "\u2212"; // minus sign instead of hyphen; visually balanced with +
span.className = "diff-marker-delete";
} else {
// Non-breaking space keeps this span from collapsing so
// subsequent +/- markers stay vertically aligned with their code lines.
span.textContent = "\u00A0";
}
markerEl.appendChild(span);
}

const topOffset =
pre.offsetTop + parseFloat(getComputedStyle(pre).paddingTop);
markerEl.style.top = `${topOffset}px`;

container.appendChild(markerEl);
}

function overrideDiffCopyButton(container, codeElement) {
const copyButton = container.querySelector(".copy-to-clipboard-button");
if (!copyButton) return;

const timeout =
parseInt(codeElement.getAttribute("data-prismjs-copy-timeout")) || 5000;

const newButton = copyButton.cloneNode(true);
copyButton.parentNode.replaceChild(newButton, copyButton);

newButton.addEventListener("click", async () => {
const cleanText = getCleanDiffText(codeElement.textContent);
const span = newButton.querySelector("span");

try {
await navigator.clipboard.writeText(cleanText);
if (span) {
span.textContent = "Copied!";
newButton.setAttribute("data-copy-state", "copy-success");
}
} catch {
if (span) {
span.textContent = "Press Ctrl+C to copy";
newButton.setAttribute("data-copy-state", "copy-error");
}
}

if (span) {
setTimeout(() => {
span.textContent = "Copy";
newButton.setAttribute("data-copy-state", "copy");
}, timeout);
}
});
}

async function highlightElement(
importLanguage,
languageImports,
codeElement,
hideCopyButton,
) {
if (typeof window !== "undefined") {
// Only import the language if it hasn't been imported before.
if (!languageImports.has(importLanguage)) {
const isDiff = importLanguage.startsWith("diff-");
if (isDiff) {
const baseLang = importLanguage.substring(5);
for (const lang of ["diff", baseLang]) {
if (!languageImports.has(lang)) {
try {
await import(`prismjs/components/prism-${lang}`);
languageImports.set(lang, true);
} catch (error) {
console.error(`Prism doesn't support this language: ${lang}`);
}
}
}
// Prism's diff grammar only recognizes +/- prefixes. We use "=" for
// unchanged lines to keep code aligned (no visual shift) and to preserve
// leading whitespace that markdown processing would otherwise strip.
Comment on lines +278 to +280
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘

if (!Prism.languages.diff["unchanged-equal"]) {
Prism.languages.diff["unchanged-equal"] = {
pattern: /^(?:=.*(?:\r\n?|\n|(?![\s\S])))+/m,
alias: ["unchanged"],
inside: {
line: {
pattern: /(.)(?=[\s\S]).*(?:\r\n?|\n)?/,
lookbehind: true,
},
prefix: {
pattern: /[\s\S]/,
alias: "unchanged",
},
},
};
Prism.languages.diff.PREFIXES["unchanged-equal"] = "=";
}
languageImports.set(importLanguage, true);
} else if (!languageImports.has(importLanguage)) {
try {
await import(`prismjs/components/prism-${importLanguage}`);
languageImports.set(importLanguage, true);
Expand All @@ -185,16 +305,18 @@ async function highlightElement(
}
}

// Highlight the code block and conditionally enable toolbar plugins (including copy button)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Are these comments intentionally removed?

if (codeElement) {
// First highlight the element
Prism.highlightElement(codeElement);

// Then activate toolbar plugins on the parent container if copy button is not hidden
if (!hideCopyButton) {
const container = codeElement.closest(`.${styles.Container}`);
if (container) {
Prism.highlightAllUnder(container);

if (isDiff) {
addDiffMarkers(container, codeElement);
overrideDiffCopyButton(container, codeElement);
}
}
}
}
Expand Down
57 changes: 57 additions & 0 deletions components/blocks/code.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,57 @@
@apply p-6 text-white font-medium relative leading-relaxed;
}

/* Must match .Pre code typography so markers align line-by-line */
.DiffMarkers {
position: absolute;
left: 0;
width: 1.5rem;
text-align: center;
pointer-events: none;
@apply font-mono leading-relaxed;
}

/* Fade overlays on Container (outside scroll area so they don't scroll) */
.Container::before,
.Container::after {
content: "";
position: absolute;
bottom: 0.75rem;
width: 1.5rem;
@apply z-sidebar;
pointer-events: none;
}

.Container:has(.Header)::before,
.Container:has(.Header)::after {
top: 2.5rem;
}

.Container:not(:has(.Header))::before,
.Container:not(:has(.Header))::after {
top: 0;
}

.Container::before {
left: 0;
background: linear-gradient(
to right,
theme("colors.gray.90"),
theme("colors.gray.90") 0.375rem,
transparent
);
}

.Container::after {
right: 0;
background: linear-gradient(
to left,
theme("colors.gray.90"),
theme("colors.gray.90") 0.375rem,
transparent
);
}

/* Keep in sync with components/blocks/autofunction.module.css */
.Pre,
.Container code {
Expand All @@ -47,6 +98,7 @@
@apply absolute top-0 right-0
flex items-center justify-end
px-3 h-10
z-header
text-gray-80 text-xs font-medium tracking-wide;
}

Expand Down Expand Up @@ -122,6 +174,11 @@
}
}

:global(.refcard) .Container::before,
:global(.refcard) .Container::after {
display: none;
}

:global(.refcard) .Container code {
@apply block p-4 h-full;
}
Expand Down
2 changes: 1 addition & 1 deletion content/deploy/community-cloud/get-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ You will sign in to your GitHub account during this process. Community Cloud wil

1. Go to the app's entrypoint file (`streamlit_app.py`) in the left pane, and change line 3 by adding "Streamlit" inside `st.title`.

```diff
```diff-python
-st.title("🎈 My new app")
+st.title("🎈 My new Streamlit app")
```
Expand Down
16 changes: 8 additions & 8 deletions content/develop/tutorials/authentication/google.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,10 +207,10 @@ To create an app with user authentication, you'll need to configure your secrets

<Note>
If you don't want to use a callback, you can replace the last line with an equivalent `if` statement:
```diff
- st.button("Log in with Google", on_click=st.login)
+ if st.button("Log in with Google"):
+ st.login()
```diff-python
- st.button("Log in with Google", on_click=st.login)
+ if st.button("Log in with Google"):
+ st.login()
```
</Note>

Expand All @@ -233,10 +233,10 @@ To create an app with user authentication, you'll need to configure your secrets

1. Replace `st.user` with a personalized greeting:

```diff
else:
- st.user
+ st.header(f"Welcome, {st.user.name}!")
```diff-python
=else:
- st.user
+ st.header(f"Welcome, {st.user.name}!")
```

1. Add a logout button:
Expand Down
16 changes: 8 additions & 8 deletions content/develop/tutorials/authentication/microsoft.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,10 @@ To create an app with user authentication, you'll need to configure your secrets

<Note>
If you don't want to use a callback, you can replace the last line with an equivalent `if` statement:
```diff
- st.button("Log in with Microsoft", on_click=st.login)
+ if st.button("Log in with Microsoft"):
+ st.login()
```diff-python
- st.button("Log in with Microsoft", on_click=st.login)
+ if st.button("Log in with Microsoft"):
+ st.login()
```
</Note>
Expand All @@ -219,10 +219,10 @@ To create an app with user authentication, you'll need to configure your secrets

1. Replace `st.user` with a personalized greeting:

```diff
else:
- st.user
+ st.header(f"Welcome, {st.user.name}!")
```diff-python
=else:
- st.user
+ st.header(f"Welcome, {st.user.name}!")
```

1. Add a logout button:
Expand Down
Loading