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
3 changes: 3 additions & 0 deletions src/terms/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
load_dotenv('.env')

base_url = os.getenv('BASE_URL', '/')

if os.getenv('CATALOG_PATH') is None:
raise RuntimeError("CATALOG_PATH environment variable must be set")
catalog_path = Path(os.getenv('CATALOG_PATH'))
public_path = Path(os.getenv('PUBLIC_PATH', 'public'))

Expand Down
138 changes: 138 additions & 0 deletions src/terms/elements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
from collections.abc import Iterable
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from xml.etree import ElementTree as et

from terms.config import module_map, ns_dc
from terms.utils import get_git_info


@dataclass
class Reference:
uri: str
url: str | None = None

def to_serializable(self) -> dict[str, str]:
return {'uri': self.uri, 'url': self.url}


@dataclass
class Element:
uri: str
type: str
module: str
attributes: dict[str, Any] = field(default_factory=dict)
url: str | None = None
git: dict[str, Any] | None = None

def to_serializable(self) -> dict[str, Any]:
base = {'uri': self.uri, 'type': self.type, 'module': self.module, 'url': self.url}
if self.git:
base['git'] = self.git
base.update({key: self._serialize_value(value) for key, value in self.attributes.items()})
return base

def _serialize_value(self, value: Any) -> Any:
if isinstance(value, Reference):
return value.to_serializable()
if isinstance(value, list):
return [self._serialize_value(item) for item in value]
return value


def gather_elements(files: list[Path]) -> list:
elements = []
urls = {}
git_infos: dict[Path, dict[str, Any] | None] = {}
for file_path in files:
tree = et.parse(file_path)
root_node = tree.getroot()

git_info = git_infos.get(file_path)
if file_path not in git_infos:
git_info = get_git_info(file_path)
git_infos[file_path] = git_info

for element_node in root_node:
uri = element_node.attrib.get(f"{ns_dc}uri")
element = Element(
uri=uri,
type=element_node.tag,
module=module_map[element_node.tag],
git=git_info,
)

for child_node in element_node:
key = _extract_key(child_node)

if len(child_node) > 0:
element.attributes[key] = [
Reference(uri=grand_child_node.attrib.get(f"{ns_dc}uri"))
for grand_child_node in child_node
]
elif child_node.attrib.get(f"{ns_dc}uri"):
element.attributes[key] = Reference(uri=child_node.attrib.get(f"{ns_dc}uri"))
else:
element.attributes[key] = child_node.text

element.url = f"{element.module}/{element.attributes.get('uri_path', element.attributes.get('path', ''))}"
urls[element.uri] = element.url
elements.append(element)

# loop over subvalues again and add urls
for element in elements:
_attach_urls(element.attributes.values(), urls)

return sorted([element.to_serializable() for element in elements], key=lambda x: x["uri"])


def _extract_key(child_node: et.Element) -> str:
if child_node.tag.startswith(ns_dc):
return child_node.tag[len(ns_dc):]
if 'lang' in child_node.attrib:
return f"{child_node.tag}_{child_node.attrib['lang']}"
return child_node.tag


def _attach_urls(values: Iterable[Any], urls: dict[str, str]) -> None:
for value in values:
if isinstance(value, list):
for item in value:
if isinstance(item, Reference):
item.url = urls.get(item.uri)
elif isinstance(value, Reference):
value.url = urls.get(value.uri)


def build_element_tree(
root_uri: str, elements_by_uri: dict[str, dict[str, Any]], seen: set[str] | None = None
) -> dict[str, Any] | None:
"""Build a nested tree representation for a catalog starting at ``root_uri``."""

seen = set() if seen is None else seen

element = elements_by_uri.get(root_uri)
if not element or root_uri in seen:
return None

seen.add(root_uri)

tree = {
"uri": element["uri"],
"type": element.get("type") or element.get("module") or "element",
"url": element.get("url"),
"children": [],
}

for key in ["sections", "pages", "questionsets", "questions"]:
for child_ref in element.get(key, []) or []:
child_uri = child_ref.get("uri")
if not child_uri:
continue

child_tree = build_element_tree(child_uri, elements_by_uri, seen)
if child_tree:
tree["children"].append(child_tree)

return tree
31 changes: 20 additions & 11 deletions src/terms/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
from http.server import HTTPServer, SimpleHTTPRequestHandler

import typer
from dotenv import load_dotenv

from .config import base_url, catalog_path, public_path
from .utils import copy_static, download_assets, gather_elements, get_template

load_dotenv('.env')
from .elements import build_element_tree, gather_elements
from .utils import copy_static, download_assets, gather_files, get_template_env

app = typer.Typer()
template_env = get_template_env()

@app.command()
def build():
Expand All @@ -26,9 +25,9 @@ def build():

@app.command()
def index():
template = get_template('index.html')

elements = gather_elements(catalog_path)
template = template_env.get_template('index.html')
xml_files = gather_files(catalog_path)
elements = gather_elements(xml_files)

html = template.render(base_url=base_url, elements=elements)
html_path = public_path / 'index.html'
Expand All @@ -40,11 +39,12 @@ def index():
@app.command()
def elements():
module_elements = defaultdict(list)
for element in gather_elements(catalog_path):
xml_files = gather_files(catalog_path)
for element in gather_elements(xml_files):
module_elements[element['module']].append(element)

for module, elements in module_elements.items():
template = get_template('elements.html')
template = template_env.get_template('elements.html')

html = template.render(base_url=base_url, module=module, elements=elements)
html_path = public_path / module
Expand All @@ -55,8 +55,17 @@ def elements():

@app.command()
def element():
for element in gather_elements(catalog_path):
template = get_template('element.html')
xml_files = gather_files(catalog_path)
elements = gather_elements(xml_files)
elements_by_uri = {element["uri"]: element for element in elements}

for element in elements:
if element.get("type") == "catalog":
tree = build_element_tree(element["uri"], elements_by_uri)
if tree:
element["tree"] = tree

template = template_env.get_template('element.html')

html = template.render(base_url=base_url, element=element)
html_path = public_path / element['module'] / element.get('uri_path', element.get('path', ''))
Expand Down
11 changes: 9 additions & 2 deletions src/terms/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
--rdmo-color-link-hover: #2a5d90;
--rdmo-color-light-gray: #f8f9fa;
--rdmo-color-dark-gray: #999;
--rdmo-color-header: #fff;
--rdmo-color-header-bg: #000;
--rdmo-color-header: #fff; --rdmo-color-header-bg: #000;
--rdmo-color-header-height: 300px;
--rdmo-color-footer: #999;
--rdmo-color-footer-bg: #001;
Expand Down Expand Up @@ -45,6 +44,10 @@ nav.navbar .filter-form {
}
}

nav.navbar #language-toggle {
min-width: 8rem;
}

.navbar-toggler-icon {
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
Expand All @@ -53,3 +56,7 @@ main {
margin-top: 6rem;
margin-bottom: 6rem;
}

.footer {
margin-top: auto;
}
61 changes: 60 additions & 1 deletion src/terms/static/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,55 @@ document.addEventListener("DOMContentLoaded", () => {
const filterInput = document.getElementById("filter")
if (!filterInput) return

const languageToggle = document.getElementById("language-toggle")
const languageItems = Array.prototype.slice.call(
document.querySelectorAll("[data-lang]")
)

const availableLanguages = Array.from(
new Set(
languageItems
.map((item) => item.getAttribute("data-lang"))
.filter((lang) => lang)
)
).sort()

const addLanguageOptions = () => {
if (!languageToggle || availableLanguages.length === 0) return

const current = languageToggle.value
availableLanguages.forEach((lang) => {
if (!languageToggle.querySelector(`option[value="${lang}"]`)) {
const option = document.createElement("option")
option.value = lang
option.textContent = lang.toUpperCase()
languageToggle.appendChild(option)
}
})

const preferred = localStorage.getItem("rdmo-terms-language") || (availableLanguages.includes("en") ? "en" : current)
if (preferred && languageToggle.querySelector(`option[value="${preferred}"]`)) {
languageToggle.value = preferred
}
}

const applyLanguage = (lang) => {
if (!languageToggle) return

localStorage.setItem("rdmo-terms-language", lang)

languageItems.forEach((item) => {
const itemLang = item.getAttribute("data-lang")
if (!itemLang) return

if (lang === "all" || itemLang === lang) {
item.classList.remove("d-none")
} else {
item.classList.add("d-none")
}
})
}

// Collect all result cards on this page
const elements = Array.prototype.slice.call(
document.querySelectorAll(".element[data-uri]")
Expand Down Expand Up @@ -103,7 +152,7 @@ document.addEventListener("DOMContentLoaded", () => {
return
}

const options = {
let options = {
prefix: true,
fuzzy: 0.2
}
Expand Down Expand Up @@ -132,4 +181,14 @@ document.addEventListener("DOMContentLoaded", () => {
const debouncedInput = _.debounce(handleInput, 300)

filterInput.addEventListener("input", debouncedInput)

addLanguageOptions()
applyLanguage(languageToggle ? languageToggle.value : "all")

if (languageToggle) {
languageToggle.addEventListener("change", (event) => {
applyLanguage(event.target.value)
})
}

})
Loading