fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / test (push) Failing after 24m19s
FlowDeck CI / docker (push) Skipped

Corrige deux régressions de /local-workspace :

- le chemin du header restait bloqué sur « Home / <workspace> » quel que
  soit le dossier affiché : la route rend désormais breadcrumb_items
  (Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
  collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
  le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
  fois : Alpine.data('wsInitData') retournait le même objet singleton, le
  2e montage (navigation partielle) levait « Cannot redefine property:
  \ » et initTree abandonnait, laissant tout le contenu au state
  brut. La factory retourne désormais une enveloppe fraîche par montage
  qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
  exécution (le 2e montage gardait le folder_id du 1er chargement).

Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
This commit is contained in:
2026-10-09 16:58:04 -04:00
parent 7468e497c6
commit 0218d8f5e5
44 changed files with 12060 additions and 1729 deletions
+224 -23
View File
@@ -66,28 +66,226 @@ def _block_text(b: dict) -> str:
# ── Source resolution: read real textual content for ANY page type ──
# Extensions whose content is plain text / code / markdown (textual exportable).
# Mirrors static/js/code-highlight.js EXT_TO_LANG (single source of truth for
# the viewer lives client-side; this set gates server-side text handling:
# file viewer, copy-to-clipboard, exports, imports).
_TEXTUAL_EXTS = {
"md", "markdown", "txt", "log", "text",
"py", "js", "ts", "jsx", "tsx", "html", "htm", "css", "json", "xml",
"yaml", "yml", "toml", "ini", "cfg", "conf", "env", "sh", "bash", "zsh",
"ps1", "bat", "cmd", "rb", "go", "rs", "java", "c", "cpp", "h", "hpp",
"php", "swift", "kt", "scala", "sql", "r", "vue", "svelte", "astro",
"properties", "gitignore", "dockerfile", "makefile",
"md", "markdown", "mkd", "mdown", "mdwn", "rmd", "qmd",
"txt", "log", "text", "csv", "tsv", "tab", "nfo", "asc",
"py", "pyw", "pyi", "gyp", "gypi",
"js", "mjs", "cjs", "jsx", "ts", "mts", "cts", "tsx",
"html", "htm", "xhtml", "xht", "xml", "xsd", "xsl", "xslt",
"rss", "atom", "plist", "wsf", "svg", "axml",
"vue", "svelte", "astro",
"css", "scss", "sass", "less",
"json", "json5", "jsonc", "geojson", "webmanifest", "har", "avsc",
"yaml", "yml", "toml", "ini", "cfg", "conf", "env", "properties",
"inf", "reg", "url", "prop",
"gitconfig", "gitmodules", "editorconfig",
"sh", "bash", "zsh", "ksh", "mksh", "yash", "fish",
"ps1", "psm1", "psd1", "bat", "cmd",
"rb", "gemspec", "rake", "rbw",
"go", "rs", "java", "jsp", "kt", "kts",
"scala", "sc", "groovy", "gvy", "gradle",
"c", "cpp", "cc", "cxx", "c++", "hh", "hxx", "hcc", "h", "hpp",
"cs", "csx", "cake", "ino", "pde",
"php", "phtml", "swift", "sql", "pgsql", "psql", "mysql", "mssql",
"r", "jl", "lua", "dart",
"pl", "pm", "pod",
"hs", "lhs", "ex", "exs", "eex", "heex", "leex",
"erl", "hrl", "escript",
"clj", "cljs", "cljc", "edn",
"ml", "mli", "fs", "fsi", "fsx", "fsscript",
"nim", "nims", "d", "di",
"m", "mm", "vb", "vbs", "bas", "pas", "pp", "dpr",
"vhd", "vhdl", "v", "vh", "sv", "svh",
"asm", "s", "nasm", "inc",
"ada", "ads", "adb",
"f", "for", "f90", "f95", "f03", "f08",
"cob", "cbl", "cpy",
"dockerfile", "containerfile", "tf", "tfvars", "hcl",
"cmake", "makefile", "mk", "mak", "ninja",
"gql", "graphql", "graphqls", "proto", "thrift",
"vim", "vimrc", "gvimrc", "awk", "gcode", "gco", "nc", "ngc",
"http", "rest", "nginx", "apache", "htaccess", "wat",
"diff", "patch", "rej",
"tex", "latex", "sty", "cls", "dtx", "bib", "bbl",
"gitignore", "gitattributes", "gitkeep", "dockerignore", "npmignore",
"lock", "mod", "sum",
}
# Well-known extensionless / dotfile basenames that are textual code.
_TEXTUAL_FILENAMES = {
"dockerfile", "containerfile", "makefile", "gnumakefile",
"cmakelists.txt", "rakefile", "gemfile", "vagrantfile",
"berksfile", "brewfile", "dangerfile", "fastfile", "podfile",
"go.mod", "go.sum",
".bashrc", ".bash_profile", ".bash_login", ".bash_logout",
".zshrc", ".zshenv", ".profile", ".shrc", ".kshrc",
".vimrc", ".gvimrc", ".viminfo",
".gitconfig", ".editorconfig", ".htaccess",
}
# Exact basenames → highlight.js id (checked before the extension).
_FILENAME_LANG = {
"dockerfile": "dockerfile", "containerfile": "dockerfile",
"makefile": "makefile", "gnumakefile": "makefile",
"cmakelists.txt": "cmake",
"rakefile": "ruby", "gemfile": "ruby", "vagrantfile": "ruby",
"berksfile": "ruby", "brewfile": "ruby", "dangerfile": "ruby",
"fastfile": "ruby", "podfile": "ruby",
"go.mod": "go",
"nginx.conf": "nginx",
".bashrc": "bash", ".bash_profile": "bash", ".bash_login": "bash",
".bash_logout": "bash", ".zshrc": "bash", ".zshenv": "bash",
".profile": "bash", ".shrc": "bash", ".kshrc": "bash",
".vimrc": "vim", ".gvimrc": "vim",
".gitconfig": "ini", ".editorconfig": "ini", ".htaccess": "apache",
}
_CODE_LANG = {
"py": "python", "js": "javascript", "ts": "typescript", "jsx": "javascript",
"tsx": "typescript", "html": "html", "htm": "html", "css": "css",
"json": "json", "xml": "xml", "yaml": "yaml", "yml": "yaml",
"toml": "toml", "ini": "ini", "cfg": "ini", "conf": "ini", "env": "ini",
"sh": "bash", "bash": "bash", "zsh": "bash", "ps1": "powershell",
"bat": "batch", "cmd": "batch", "rb": "ruby", "go": "go", "rs": "rust",
"java": "java", "c": "c", "cpp": "cpp", "h": "c", "hpp": "cpp",
"php": "php", "swift": "swift", "kt": "kotlin", "scala": "scala",
"sql": "sql", "r": "r", "vue": "html", "svelte": "html",
"astro": "html", "properties": "ini", "md": "markdown",
"markdown": "markdown", "txt": "plaintext", "log": "plaintext",
"text": "plaintext",
"py": "python", "pyw": "python", "pyi": "python",
"gyp": "python", "gypi": "python",
"js": "javascript", "mjs": "javascript", "cjs": "javascript",
"jsx": "javascript",
"ts": "typescript", "mts": "typescript", "cts": "typescript",
"tsx": "typescript",
"html": "xml", "htm": "xml", "xhtml": "xml", "xht": "xml",
"xml": "xml", "xsd": "xml", "xsl": "xml", "xslt": "xml",
"rss": "xml", "atom": "xml", "plist": "xml", "wsf": "xml",
"svg": "xml", "axml": "xml",
"vue": "xml", "svelte": "xml", "astro": "xml",
"css": "css", "scss": "scss", "sass": "scss", "less": "less",
"json": "json", "json5": "json", "jsonc": "json",
"geojson": "json", "webmanifest": "json", "har": "json", "avsc": "json",
"yaml": "yaml", "yml": "yaml",
"toml": "ini", "ini": "ini", "cfg": "ini", "conf": "ini",
"env": "ini", "properties": "ini", "prop": "ini",
"inf": "ini", "reg": "ini", "url": "ini",
"gitconfig": "ini", "gitmodules": "ini", "editorconfig": "ini",
"ninja": "ini",
"md": "markdown", "markdown": "markdown", "mkd": "markdown",
"mdown": "markdown", "mdwn": "markdown", "rmd": "markdown",
"qmd": "markdown",
"tex": "latex", "latex": "latex", "sty": "latex", "cls": "latex",
"dtx": "latex",
"sql": "sql", "pgsql": "sql", "psql": "sql", "mysql": "sql",
"mssql": "sql",
"sh": "bash", "bash": "bash", "zsh": "bash", "ksh": "bash",
"mksh": "bash", "yash": "bash",
"fish": "shell",
"ps1": "powershell", "psm1": "powershell", "psd1": "powershell",
"bat": "dos", "cmd": "dos",
"rb": "ruby", "gemspec": "ruby", "rake": "ruby", "rbw": "ruby",
"go": "go", "rs": "rust",
"java": "java", "jsp": "java",
"kt": "kotlin", "kts": "kotlin",
"scala": "scala", "sc": "scala",
"groovy": "groovy", "gvy": "groovy", "gradle": "groovy",
"c": "c", "h": "c",
"cpp": "cpp", "cc": "cpp", "cxx": "cpp", "c++": "cpp",
"hh": "cpp", "hxx": "cpp", "hcc": "cpp", "hpp": "cpp",
"ino": "cpp", "pde": "cpp",
"cs": "csharp", "csx": "csharp", "cake": "csharp",
"php": "php", "phtml": "php-template",
"swift": "swift", "r": "r", "jl": "julia", "lua": "lua",
"dart": "dart", "pl": "perl", "pm": "perl", "pod": "perl",
"hs": "haskell", "lhs": "haskell",
"ex": "elixir", "exs": "elixir", "eex": "elixir",
"heex": "elixir", "leex": "elixir",
"erl": "erlang", "hrl": "erlang", "escript": "erlang",
"clj": "clojure", "cljs": "clojure", "cljc": "clojure", "edn": "clojure",
"ml": "ocaml", "mli": "ocaml",
"fs": "fsharp", "fsi": "fsharp", "fsx": "fsharp", "fsscript": "fsharp",
"nim": "nim", "nims": "nim",
"d": "d", "di": "d",
"m": "objectivec", "mm": "objectivec",
"vb": "vbnet", "vbs": "vbnet", "bas": "vbnet",
"pas": "delphi", "pp": "delphi", "dpr": "delphi",
"vhd": "vhdl", "vhdl": "vhdl",
"v": "verilog", "vh": "verilog", "sv": "verilog", "svh": "verilog",
"asm": "x86asm", "s": "x86asm", "nasm": "x86asm", "inc": "x86asm",
"ada": "ada", "ads": "ada", "adb": "ada",
"f": "fortran", "for": "fortran", "f90": "fortran", "f95": "fortran",
"f03": "fortran", "f08": "fortran",
"cob": "plaintext", "cbl": "plaintext", "cpy": "plaintext",
"dockerfile": "dockerfile",
"tf": "plaintext", "tfvars": "plaintext", "hcl": "plaintext",
"cmake": "cmake",
"makefile": "makefile", "mk": "makefile", "mak": "makefile",
"gql": "graphql", "graphql": "graphql", "graphqls": "graphql",
"proto": "protobuf", "thrift": "thrift",
"vim": "vim", "vimrc": "vim", "gvimrc": "vim",
"awk": "awk",
"gcode": "gcode", "gco": "gcode", "nc": "gcode", "ngc": "gcode",
"http": "http", "rest": "http",
"nginx": "nginx", "apache": "apache", "htaccess": "apache",
"wat": "wasm",
"diff": "diff", "patch": "diff", "rej": "diff",
"bib": "plaintext", "bbl": "plaintext",
"txt": "plaintext", "log": "plaintext", "text": "plaintext",
"csv": "plaintext", "tsv": "plaintext", "tab": "plaintext",
"nfo": "plaintext", "asc": "plaintext",
"lock": "plaintext",
"gitignore": "plaintext", "gitattributes": "plaintext",
"gitkeep": "plaintext", "dockerignore": "plaintext",
"npmignore": "plaintext",
}
# Aliases users may type in markdown fences / old block labels.
_CODE_ALIASES = {
"py": "python", "js": "javascript", "jsx": "javascript",
"ts": "typescript", "tsx": "typescript",
"html": "xml", "htm": "xml", "markup": "xml",
"yml": "yaml", "md": "markdown",
"sh": "bash", "zsh": "bash", "console": "shell",
"ps1": "powershell", "ps": "powershell",
"bat": "dos", "cmd": "dos", "batch": "dos",
"c++": "cpp", "c#": "csharp", "cs": "csharp",
"kt": "kotlin", "rb": "ruby", "rs": "rust", "pl": "perl",
"toml": "ini", "cfg": "ini", "conf": "ini", "env": "ini",
"tex": "latex", "docker": "dockerfile", "make": "makefile",
"plain text": "plaintext", "plain": "plaintext", "text": "plaintext",
"none": "plaintext", "": "plaintext",
}
# highlight.js ids served by static/js/highlight.min.js + highlight-extra.min.js.
_HLJS_LANGS = frozenset({
"ada", "apache", "armasm", "awk", "bash", "c", "clojure", "cmake", "cpp",
"csharp", "css", "d", "dart", "delphi", "diff", "dockerfile", "dos",
"elixir", "erlang", "fsharp", "fortran", "gcode", "go",
"graphql", "groovy", "haskell", "http", "ini", "java", "javascript",
"json", "julia", "kotlin", "latex", "less", "lua", "makefile",
"markdown", "matlab", "nginx", "nim", "objectivec", "ocaml", "perl",
"php", "php-template", "plaintext", "powershell", "protobuf", "python",
"python-repl", "r", "ruby", "rust", "scala", "scss", "shell", "sql",
"stan", "stata", "swift", "thrift", "typescript", "vbnet", "verilog",
"vhdl", "vim", "wasm", "x86asm", "xml", "yaml",
})
def normalize_code_lang(lang: str | None) -> str:
"""Normalize any code language label to a bundled highlight.js id.
Accepts highlight.js ids, file extensions, markdown fence names and the
legacy ``Plain Text`` label. Unknown values degrade to ``plaintext``.
"""
lang = (lang or "").strip().lower()
if not lang or lang in ("plain text", "none"):
return "plaintext"
if lang in _CODE_ALIASES:
lang = _CODE_ALIASES[lang]
if lang in _CODE_LANG.values() or lang in _HLJS_LANGS:
return lang
return "plaintext"
def code_lang_for_file(filename: str | None) -> str:
"""Best highlight.js id for an uploaded / repo file name."""
name = (filename or "").replace("\\", "/").rsplit("/", 1)[-1].lower()
if not name:
return "plaintext"
if name in _FILENAME_LANG:
return _FILENAME_LANG[name]
ext = name.rsplit(".", 1)[-1] if "." in name else ""
return _CODE_LANG.get(ext, "plaintext")
_MARKDOWN_MIMES = {"text/markdown", "text/x-markdown", "application/octet-stream"}
@@ -119,7 +317,8 @@ def _file_text(page: dict) -> str | None:
name = (rel.rsplit("/", 1)[-1] or "").lower()
ext = name.rsplit(".", 1)[-1] if "." in name else ""
mime = (meta.get("mime_type") or "").lower()
if not (ext in _TEXTUAL_EXTS or mime.startswith("text/")):
if not (ext in _TEXTUAL_EXTS or name in _TEXTUAL_FILENAMES
or mime.startswith("text/")):
return None
try:
full = (_data_root() / rel).resolve()
@@ -161,8 +360,7 @@ def _page_source(page: dict):
mime = (meta.get("mime_type") or "").lower()
if ext in ("md", "markdown") or mime in _MARKDOWN_MIMES or mime.startswith("text/markdown"):
return "md", text
lang = _CODE_LANG.get(ext, "plaintext")
return "code", (text, lang)
return "code", (text, code_lang_for_file(name))
# Unknown format (e.g. legacy) -> try to dump as raw text
if content.strip():
@@ -497,7 +695,7 @@ def blocks_to_markdown(blocks: list) -> str:
out.append("---")
elif t == "code":
lang = b.get("language") or ""
out.append(f"```{lang}\n{c}\n```")
out.append(f"```{normalize_code_lang(lang) if lang else ''}\n{c}\n```")
elif t == "toggle":
out.append(f"### {c}")
if b.get("children"):
@@ -615,8 +813,11 @@ def blocks_to_html(blocks: list) -> str:
parts.append("<hr>")
elif t == "code":
lang = b.get("language") or ""
hljs_lang = normalize_code_lang(lang)
label = f'<div class="code-lang">{_text(lang)}</div>' if lang else ""
parts.append(f"<pre>{label}<code>{c}</code></pre>")
parts.append(
f'<pre>{label}<code class="language-{hljs_lang}">{c}</code></pre>'
)
elif t == "math":
parts.append(f'<div class="math">\\[{c}\\]</div>')
elif t == "table_of_contents":