Commit c9c6a73e authored by Kantz's avatar Kantz
Browse files

Quellen wieder zugreifbar

parent 8a194678
from __future__ import annotations
from typing import Dict, Iterable, Tuple
from typing import Dict, Iterable, Protocol, Set, Tuple
import re
from urllib.parse import quote
from app.deterministic_services import Source, SourceID
_BRACKET_RE = re.compile(r"\[(?P<ref>[^\[\]]+)\]")
def _source_id_key(source_id: SourceID) -> str:
class _SourceIDLike(Protocol):
chapter_title: str | None
section_title: str | None
subsection_title: str | None
title: str | None
doc_type: str | None
def to_string(self) -> str: ...
class _SourceLike(Protocol):
source_id: _SourceIDLike
def _source_id_key(source_id: _SourceIDLike) -> str:
parts = [
source_id.chapter_title or "",
source_id.section_title or "",
......@@ -30,9 +42,11 @@ def _normalize_ref_token(token: str) -> str:
return t.casefold()
def _build_source_index(sources: Iterable[Source]) -> Dict[str, str]:
def _build_source_index(sources: Iterable[_SourceLike]) -> Tuple[Dict[str, str], Set[str]]:
index: Dict[str, str] = {}
doc_types: Set[str] = set()
for source in sources:
doc_types.add(_normalize_ref_token(source.source_id.doc_type or ""))
key = _source_id_key(source.source_id)
normalized_key = _normalize_ref_token(key)
index.setdefault(normalized_key, key)
......@@ -46,22 +60,46 @@ def _build_source_index(sources: Iterable[Source]) -> Dict[str, str]:
source.source_id.subsection_title,
source.source_id.title,
]
filtered = [p for p in parts if p and p.lower() != "none"]
if filtered:
minimal = "|".join(filtered + [source.source_id.doc_type])
index.setdefault(_normalize_ref_token(minimal), key)
for start in range(len(parts)):
filtered = [
p for p in parts[start:]
if p and p.lower() != "none"
]
if filtered:
minimal = "|".join(filtered + [source.source_id.doc_type])
index.setdefault(_normalize_ref_token(minimal), key)
if source.source_id.title:
compact = f"{source.source_id.title}|{source.source_id.doc_type}"
index.setdefault(_normalize_ref_token(compact), key)
return index
return index, {dtype for dtype in doc_types if dtype}
def _candidate_tokens(token: str, doc_types: Set[str]) -> Iterable[str]:
normalized = _normalize_ref_token(token)
yield normalized
parts = [part for part in normalized.split("|") if part]
if not parts:
return
for idx, part in enumerate(parts):
if part in doc_types:
yield "|".join(parts[: idx + 1])
if len(parts) >= 2:
yield "|".join(parts[-2:])
if len(parts) >= 3:
yield "|".join(parts[-3:])
if len(parts) >= 4:
yield "|".join(parts[-4:])
def decode_references(text: str, sources: Iterable[Source]) -> Tuple[str, int]:
def decode_references(text: str, sources: Iterable[_SourceLike]) -> Tuple[str, int]:
if not text:
return text, 0
index = _build_source_index(sources)
index, doc_types = _build_source_index(sources)
if not index:
return text, 0
......@@ -75,8 +113,11 @@ def decode_references(text: str, sources: Iterable[Source]) -> Tuple[str, int]:
if end < len(text) and text[end:end + 1] == "(":
return match.group(0)
normalized = _normalize_ref_token(ref)
key = index.get(normalized)
key = None
for candidate in _candidate_tokens(ref, doc_types):
key = index.get(candidate)
if key:
break
if not key:
return match.group(0)
......
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import importlib.util
import unittest
_MODULE_PATH = (
Path(__file__).resolve().parents[1]
/ "app"
/ "deterministic_services"
/ "referenz_decoder.py"
)
_SPEC = importlib.util.spec_from_file_location("referenz_decoder", _MODULE_PATH)
assert _SPEC is not None and _SPEC.loader is not None
_MODULE = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(_MODULE)
decode_references = _MODULE.decode_references
@dataclass
class SourceIDStub:
chapter_title: str | None
section_title: str | None
subsection_title: str | None
title: str
doc_type: str
def to_string(self) -> str:
parts = [self.title]
if self.subsection_title:
parts.insert(0, self.subsection_title)
if self.section_title:
parts.insert(0, self.section_title)
if self.chapter_title:
parts.insert(0, self.chapter_title)
return f"[{'|'.join(parts)}|{self.doc_type}]"
@dataclass
class SourceStub:
source_id: SourceIDStub
def _mk_source() -> SourceStub:
return SourceStub(
source_id=SourceIDStub(
chapter_title="Kapitel 1",
section_title="Lineare Funktionen",
subsection_title="Steigung",
title="Definition",
doc_type="subsection",
)
)
class ReferenzDecoderUnitTest(unittest.TestCase):
def test_decodes_reference_without_chapter(self) -> None:
source = _mk_source()
text = "Siehe [Lineare Funktionen|Steigung|Definition|subsection]."
decoded, count = decode_references(text, [source])
self.assertEqual(count, 1)
self.assertIn(
"(doc://Kapitel%201%7CLineare%20Funktionen%7CSteigung%7CDefinition%7Csubsection)",
decoded,
)
def test_decodes_reference_with_extra_tail_segment(self) -> None:
source = _mk_source()
text = "Siehe [Lineare Funktionen/Steigung/Definition | subsection | ...]."
decoded, count = decode_references(text, [source])
self.assertEqual(count, 1)
self.assertIn(
"(doc://Kapitel%201%7CLineare%20Funktionen%7CSteigung%7CDefinition%7Csubsection)",
decoded,
)
def test_skips_existing_markdown_links(self) -> None:
source = _mk_source()
text = "Bereits verlinkt: [Definition](doc://foo)."
decoded, count = decode_references(text, [source])
self.assertEqual(count, 0)
self.assertEqual(decoded, text)
if __name__ == "__main__":
unittest.main()
import { useEffect, useRef } from "react";
import ReactMarkdown from "react-markdown";
import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
import type { RetrievedDoc } from "../Retrieval/DocPanel";
import { t } from "../../i18n";
......@@ -54,6 +54,12 @@ export default function MessageBubble({
</div>
<div className="message-text">
<ReactMarkdown
urlTransform={(url) => {
if (url.startsWith("doc://")) {
return url;
}
return defaultUrlTransform(url);
}}
components={{
a: ({ href, children, ...props }) => {
const target = href || "";
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment