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
82 changes: 79 additions & 3 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,68 @@ plt.show()
\`\`\`
`

const VARIABLES_MYST_SAMPLE = `---
jupytext:
text_representation:
extension: .md
format_name: myst
format_version: 0.13
kernelspec:
display_name: C++17
language: C++17
name: xcpp17
learning:
objectives:
apply: [variable, "d\\xE9claration de variable", affectation]
prerequisites:
apply: [valeur, "op\\xE9ration", expression, "expression bool\\xE9enne", type, entier,
"r\\xE9el", "caract\\xE8re", "bool\\xE9en"]
---

+++ {"nbgrader": {"grade": false, "grade_id": "cell-b78562ee2ff6d72c", "locked": true, "schema_version": 3, "solution": false}}

# TP : variables et affectations

Dans la feuille précédente, nous avons effectué des calculs et observé
les résultats (type, valeur). Pour écrire des programmes, nous aurons
besoin de **stocker les résultats intermédiaires dans des variables**
pour en réutiliser les valeurs.

## Exercice 1

- Exécutez la cellule suivante :

\`\`\`{code-cell}
int a;
a = 3;
\`\`\`

+++ {"nbgrader": {"grade": false, "grade_id": "cell-77b23801c7675aeb", "locked": true, "schema_version": 3, "solution": false}}

Une fois que la variable \`a\` a été ***déclarée*** (\`int a;\`) et qu'on
lui a ***affecté*** une valeur (\`a = 3\`), on peut afficher ou
réutiliser cette valeur :

\`\`\`{code-cell}
---
nbgrader:
grade: false
grade_id: cell-cb019a2142f5c180
locked: true
schema_version: 3
solution: false
task: false
---
a
\`\`\`

\`\`\`{code-cell}
:locked: false

a + 1
\`\`\`
`

const SG_SAMPLE = `# ---
# kernelspec: {"display_name": "Python 3", "name": "python3"}
# language_info: {"name": "python"}
Expand Down Expand Up @@ -172,17 +234,31 @@ const INPUT_LANGUAGE: Record<Format, string> = {
py: 'python',
md: 'markdown',
myst: 'markdown',
myst_vars: 'markdown',
sg: 'python',
}

// ---------------------------------------------------------------------------
// App
// ---------------------------------------------------------------------------

type Format = 'py' | 'md' | 'myst' | 'sg'
type Format = 'py' | 'md' | 'myst' | 'myst_vars' | 'sg'

const SAMPLES: Record<Format, string> = { py: PY_SAMPLE, md: MD_SAMPLE, myst: MYST_SAMPLE, sg: SG_SAMPLE }
const LABELS: Record<Format, string> = { py: '.py percent', md: '.md classic', myst: '.md MyST', sg: 'sphinx-gallery' }
const SAMPLES: Record<Format, string> = {
py: PY_SAMPLE,
md: MD_SAMPLE,
myst: MYST_SAMPLE,
myst_vars: VARIABLES_MYST_SAMPLE,
sg: SG_SAMPLE
}

const LABELS: Record<Format, string> = {
py: '.py percent',
md: '.md classic',
myst: '.md MyST (simple)',
myst_vars: '.md MyST (variables)',
sg: 'sphinx-gallery'
}

export default function App() {
const [format, setFormat] = useState<Format>('py')
Expand Down
4 changes: 2 additions & 2 deletions src/parseClassicMd.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { codeCell, markdownCell, makeNotebook, type Cell, type Notebook } from "./notebook.js";
import { parseFrontMatter } from "./utils.js";
import { parseYAMLBlock } from "./utils.js";

// ---------------------------------------------------------------------------
// Classic markdown parser
Expand Down Expand Up @@ -40,7 +40,7 @@ export function parseClassicMd(text: string): Notebook {
i++;
}
i++; // skip closing ---
notebookMeta = parseFrontMatter(fmLines);
notebookMeta = parseYAMLBlock(fmLines);
}

while (i < lines.length) {
Expand Down
28 changes: 3 additions & 25 deletions src/parseMystMd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type Cell,
type Notebook,
} from "./notebook";
import { parseFrontMatter } from "./utils";
import { parseYAMLBlock } from "./utils";

// ---------------------------------------------------------------------------
// MyST notebook format parser
Expand Down Expand Up @@ -54,28 +54,6 @@ function substituteInlineRoles(line: string): string {
return line;
}

/** Minimal YAML/JSON option parser for cell metadata.
* Handles flat key:value and key: [flow, list] only. */
function parseOptions(lines: string[]): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const line of lines) {
const m = line.match(/^([\w-]+):\s*(.*)/);
if (!m) continue;
const key = m[1];
const val = m[2].trim();
if (val.startsWith("[") || val.startsWith("{")) {
try {
result[key] = JSON.parse(val);
} catch {
result[key] = val;
}
} else {
result[key] = val;
}
}
return result;
}

function stripTrailingBlank(lines: string[]): string[] {
let end = lines.length;
while (end > 0 && lines[end - 1].trim() === "") end--;
Expand Down Expand Up @@ -109,7 +87,7 @@ export function parseMystMd(text: string): Notebook {
i++;
}
i++; // skip closing ---
notebookMeta = parseFrontMatter(fmLines);
notebookMeta = parseYAMLBlock(fmLines);
}

while (i < lines.length) {
Expand Down Expand Up @@ -182,7 +160,7 @@ export function parseMystMd(text: string): Notebook {
}
}

const cellMeta = parseOptions(optionLines);
const cellMeta = parseYAMLBlock(optionLines);

// Skip blank line after options
if (i < lines.length && lines[i].trim() === "") i++;
Expand Down
49 changes: 37 additions & 12 deletions src/parsePy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type Cell,
type Notebook,
} from "./notebook";
import { parseFrontMatter } from "./utils";
import { parseYAMLBlock } from "./utils";

// ---------------------------------------------------------------------------
// Percent format parser
Expand Down Expand Up @@ -37,22 +37,47 @@ function parseCellHeader(rest: string): CellHeader {
s = s.replace(TYPE_TAG_RE, "").trim();
}

const metadata: Record<string, unknown> = {};
let metadata: Record<string, unknown> = {};

// Parse tags="[...]" or tags=['...']
const tagsMatch = s.match(/\btags\s*=\s*(['"])\[(.+?)\]\1/);
if (tagsMatch) {
const firstCurly = s.indexOf("{");
const firstEqual = s.indexOf("=");

if (firstCurly >= 0 && (firstEqual < 0 || firstCurly < firstEqual)) {
const jsonStr = s.slice(firstCurly);
const titleStr = s.slice(0, firstCurly).trim();
try {
metadata.tags = JSON.parse("[" + tagsMatch[2] + "]");
metadata = JSON.parse(jsonStr);
} catch {
// ignore malformed tags
// ignore
}
if (titleStr) {
metadata.name = titleStr;
}
} else {
const kvRegex = /\b([\w-]+)=("[^"]*"|'[^']*'|\[[^\]]*\]|\S+)/g;
const matches = Array.from(s.matchAll(kvRegex));
let remaining = s;
for (const match of matches) {
const key = match[1];
const valStr = match[2];
remaining = remaining.replace(match[0], "");

let parsedVal: unknown;
const strippedVal = valStr.replace(/^['"]|['"]$/g, "");
try {
const normVal = strippedVal.replace(/'/g, '"');
parsedVal = JSON.parse(normVal);
} catch {
parsedVal = strippedVal;
}
metadata[key] = parsedVal;
}
const nameStr = remaining.replace(/\s+/g, " ").trim();
if (nameStr) {
metadata.name = nameStr;
}
s = s.replace(tagsMatch[0], "").trim();
}

// Remaining text is the cell title/name
if (s) metadata.name = s;

return { cellType, metadata };
}

Expand Down Expand Up @@ -99,7 +124,7 @@ export function parsePy(text: string): Notebook {
i++;
}
i++; // skip closing # ---
notebookMeta = parseFrontMatter(fmLines);
notebookMeta = parseYAMLBlock(fmLines);
}

// Find all delimiter positions
Expand Down
4 changes: 2 additions & 2 deletions src/parseSphinxGallery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { codeCell, markdownCell, makeNotebook, type Cell, type Notebook } from "./notebook";
import { parseFrontMatter } from "./utils";
import { parseYAMLBlock } from "./utils";

// ---------------------------------------------------------------------------
// RST helpers
Expand Down Expand Up @@ -132,7 +132,7 @@ export function parseSphinxGallery(text: string): Notebook {
i++;
}
i++; // skip closing # ---
notebookMeta = parseFrontMatter(fmLines);
notebookMeta = parseYAMLBlock(fmLines);
}

// Skip any leading empty lines before checking for docstring
Expand Down
58 changes: 48 additions & 10 deletions src/toMystMd.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Notebook } from "./notebook";
import { stringifyYAML } from "./utils";
import { stringifyYAML, pythonStyleJSON, cleanCellMetadata } from "./utils";

// ---------------------------------------------------------------------------
// Notebook → MyST Markdown serializer
Expand All @@ -9,10 +9,35 @@ function joinSource(source: string[]): string {
return source.join("");
}

function serializeOptions(meta: Record<string, unknown>): string[] {
return Object.entries(meta).map(([key, val]) =>
typeof val === "string" ? `:${key}: ${val}` : `:${key}: ${JSON.stringify(val)}`,
);
/** Check if metadata has nested objects (not simple). */
function isSimpleMetadata(meta: Record<string, unknown>): boolean {
for (const val of Object.values(meta)) {
if (val && typeof val === "object" && !Array.isArray(val)) {
return false;
}
}
return true;
}

/** Serialize flat keys to MyST compact colon shorthand. */
function serializeCompactOptions(meta: Record<string, unknown>): string[] {
return Object.entries(meta).map(([key, val]) => {
if (Array.isArray(val)) {
const items = val
.map((item) => {
if (typeof item === "string" && /^[a-zA-Z0-9_-]+$/.test(item)) {
return item;
}
return JSON.stringify(item);
})
.join(", ");
return `:${key}: [${items}]`;
}
if (typeof val === "string") {
return `:${key}: ${val}`;
}
return `:${key}: ${JSON.stringify(val)}`;
});
}

/**
Expand All @@ -32,20 +57,33 @@ export function toMystMd(notebook: Notebook): string {
}

for (const cell of notebook.cells) {
const cleanMeta = cleanCellMetadata(cell.metadata);
if (cell.cell_type === "markdown") {
const hasMeta = Object.keys(cell.metadata).length > 0;
const hasMeta = Object.keys(cleanMeta).length > 0;
if (parts.length > 0 || hasMeta) {
const metaStr = hasMeta ? ` ${JSON.stringify(cell.metadata)}` : "";
const metaStr = hasMeta ? ` ${pythonStyleJSON(cleanMeta)}` : "";
parts.push(`+++${metaStr}`);
}
const src = joinSource(cell.source);
if (src) parts.push(src);
} else {
const directive = cell.cell_type === "code" ? "code-cell" : "raw-cell";
const lines: string[] = [`\`\`\`{${directive}}`];
const optionLines = serializeOptions(cell.metadata);
lines.push(...optionLines);
if (optionLines.length > 0) lines.push("");

const hasMeta = Object.keys(cleanMeta).length > 0;
if (hasMeta) {
if (isSimpleMetadata(cleanMeta)) {
const optionLines = serializeCompactOptions(cleanMeta);
lines.push(...optionLines);
lines.push("");
} else {
const yamlStr = stringifyYAML(cleanMeta);
lines.push("---");
lines.push(yamlStr);
lines.push("---");
}
}

const src = joinSource(cell.source);
if (src) lines.push(src);
lines.push("```");
Expand Down
Loading
Loading