Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
189 changes: 189 additions & 0 deletions dependencies/generate_docs_tr_ts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""
generate_docs_tr_ts.py

Reads a Doxygen-generated JSON documentation file and emits a TypeScript file
containing literal tr("...") calls so the existing i18n extractor can add the
strings to PO files.

Usage:
python3 generate_docs_tr_ts.py \
--input /path/to/json.json \
--output /path/to/ivygate/src/i18n/docs.generated.ts \
--tr-import "." \
--localizedstring-import "../util/LocalizedString" \
--export-name DOC_TR
"""

from __future__ import annotations

import argparse
import json
import os
from typing import Any, Dict, List, Optional, Set, Tuple


PREFER_EXISTING_KEYS = True


def ts_string_literal(s: str) -> str:
s = s.replace("\\", "\\\\")
s = s.replace('"', '\\"')
s = s.replace("\r", "\\r").replace("\n", "\\n")
return f'"{s}"'


def norm_text(s: Optional[str]) -> Optional[str]:
if s is None:
return None
t = s.strip()
return t if t else None


def add_entry(
entries: List[Tuple[str, str]],
seen: Set[Tuple[str, str]],
context: str,
msgid: Optional[str],
) -> None:
msgid_n = norm_text(msgid)
if not msgid_n:
return
key = (context, msgid_n)
if key in seen:
return
seen.add(key)
entries.append(key)


def get_field(obj: Dict[str, Any], text_field: str, key_field: str) -> Tuple[Optional[str], Optional[str]]:
text = norm_text(obj.get(text_field))
if not text:
return None, None
if PREFER_EXISTING_KEYS:
key = norm_text(obj.get(key_field))
if key:
return text, key
return text, None


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True, help="Path to json.json")
parser.add_argument("--output", required=True, help="Path to write docs.generated.ts")
parser.add_argument("--tr-import", default=".", help='TS import path for tr')
parser.add_argument(
"--localizedstring-import",
default="../util/LocalizedString",
help='TS import path for LocalizedString',
)
parser.add_argument("--export-name", default="DOC_TR", help="Exported const name")
args = parser.parse_args()

with open(args.input, "r", encoding="utf-8") as f:
doc = json.load(f)

entries: List[Tuple[str, str]] = []
seen: Set[Tuple[str, str]] = set()

# Functions
functions: Dict[str, Any] = doc.get("functions", {}) or {}
for fn_id, fn in functions.items():
fn_name = fn.get("name") or fn_id

brief, brief_key = get_field(fn, "brief_description", "brief_description_key")
detailed, detailed_key = get_field(fn, "detailed_description", "detailed_description_key")
ret_desc, ret_key = get_field(fn, "return_description", "return_description_key")

if brief:
add_entry(entries, seen, brief_key or f"func:{fn_name}:brief", brief)
if detailed:
add_entry(entries, seen, detailed_key or f"func:{fn_name}:detailed", detailed)
if ret_desc:
add_entry(entries, seen, ret_key or f"func:{fn_name}:return", ret_desc)

for p in fn.get("parameters", []) or []:
p_name = p.get("name") or "param"
p_desc, p_key = get_field(p, "description", "description_key")
if p_desc:
add_entry(
entries,
seen,
p_key or f"func:{fn_name}:param:{p_name}:description",
p_desc,
)

# Structures
structures: Dict[str, Any] = doc.get("structures", {}) or {}
for struct_name, struct in structures.items():
s_brief, s_brief_key = get_field(struct, "brief_description", "brief_description_key")
s_detailed, s_detailed_key = get_field(struct, "detailed_description", "detailed_description_key")

if s_brief:
add_entry(entries, seen, s_brief_key or f"struct:{struct_name}:brief", s_brief)
if s_detailed:
add_entry(entries, seen, s_detailed_key or f"struct:{struct_name}:detailed", s_detailed)

for m in struct.get("members", []) or []:
m_name = m.get("name") or "member"
m_brief, m_brief_key = get_field(m, "brief_description", "brief_description_key")
m_detailed, m_detailed_key = get_field(m, "detailed_description", "detailed_description_key")

if m_brief:
add_entry(entries, seen, m_brief_key or f"struct:{struct_name}:member:{m_name}:brief", m_brief)
if m_detailed:
add_entry(entries, seen, m_detailed_key or f"struct:{struct_name}:member:{m_name}:detailed", m_detailed)

# Enumerations
enumerations: Dict[str, Any] = doc.get("enumerations", {}) or {}
for enum_name, enum in enumerations.items():
e_brief, e_brief_key = get_field(enum, "brief_description", "brief_description_key")
e_detailed, e_detailed_key = get_field(enum, "detailed_description", "detailed_description_key")

if e_brief:
add_entry(entries, seen, e_brief_key or f"enum:{enum_name}:brief", e_brief)
if e_detailed:
add_entry(entries, seen, e_detailed_key or f"enum:{enum_name}:detailed", e_detailed)

for v in enum.get("values", []) or []:
v_name = v.get("name") or "value"
v_brief, v_brief_key = get_field(v, "brief_description", "brief_description_key")
v_detailed, v_detailed_key = get_field(v, "detailed_description", "detailed_description_key")

if v_brief:
add_entry(entries, seen, v_brief_key or f"enum:{enum_name}:value:{v_name}:brief", v_brief)
if v_detailed:
add_entry(entries, seen, v_detailed_key or f"enum:{enum_name}:value:{v_name}:detailed", v_detailed)

entries.sort(key=lambda x: (x[0], x[1]))

os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True)

lines: List[str] = []
lines.append("/* eslint-disable */")
lines.append("/**")
lines.append(" * AUTO-GENERATED FILE. DO NOT EDIT.")
lines.append(" *")
lines.append(" * Generated by generate_docs_tr_ts.py from Doxygen JSON output.")
lines.append(" */")
lines.append("")
lines.append(f"import LocalizedString from {ts_string_literal(args.localizedstring_import)};")
lines.append(f"import tr from {ts_string_literal(args.tr_import)};")
lines.append("")
lines.append(f"export const {args.export_name}: Record<string, LocalizedString> = {{")
for context, msgid in entries:
lines.append(
f" [{ts_string_literal(context)}]: "
f"tr({ts_string_literal(msgid)}, {ts_string_literal(context)}),"
)
lines.append("};")
lines.append("")

with open(args.output, "w", encoding="utf-8") as f:
f.write("\n".join(lines))

print(f"Wrote {len(entries)} doc strings to {args.output}")


if __name__ == "__main__":
main()
130 changes: 102 additions & 28 deletions dependencies/generate_doxygen_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ class FunctionParameter:
name: str
type: str
description: str | None
description_key: str | None

@dataclass
class Function:
Expand All @@ -142,9 +143,12 @@ class Function:
parameters: List[FunctionParameter]
return_type: str
return_description: str | None
return_description_key: str | None
brief_description: str | None
brief_description_key: str | None
detailed_description: str | None

detailed_description_key: str | None

@dataclass
class Module:
id: str
Expand Down Expand Up @@ -221,64 +225,134 @@ def parse_detaileddescription(node):
parameter_descriptions[parameter_name] = parse_text(item)
return parameter_descriptions

# def parse_function(node):
# global functions

# id = node.get('id')
# name = node.find('name').text
# return_type = None
# return_type_raw = node.find('type')
# # Extract last reference from return type or use entire text
# if return_type_raw is not None:
# return_type = parse_text(return_type_raw)
# refs_gen = return_type_raw.findall('ref')
# refs = [ref for ref in refs_gen]
# if len(refs) > 0:
# return_type = parse_text(refs[-1])
# # Remove EXPORT_SYM
# return_type = return_type.replace('EXPORT_SYM ', '')

# parameters = []

# detaileddescription = node.find('detaileddescription')
# detailed_description = None
# parameter_items = {}
# if detaileddescription is not None:
# parameter_items = parse_detaileddescription(detaileddescription)
# detailed_description = parse_text(detaileddescription)

# for param in node.findall('param'):
# declname = param.find('declname')
# type = param.find('type')
# if declname is None or type is None: continue
# parameters.append(FunctionParameter(
# declname.text,
# parse_text(type),
# parameter_items.get(declname.text, None)
# ))

# # Extract return description from <simplesect kind="return"> in detaileddescription
# return_description = None
# if detaileddescription is not None:
# return_description = detaileddescription.find('simplesect')
# if return_description is not None:
# return_description = parse_text(return_description)
# else:
# return_description = None

# brief_description = node.find('briefdescription')

# functions.append(Function(
# id,
# name,
# parameters,
# return_type,
# return_description,
# parse_text(brief_description) if brief_description is not None else None,
# detailed_description
# ))

def parse_function(node):
global functions

id = node.get('id')
name = node.find('name').text

# ---- Return type ----
return_type = None
return_type_raw = node.find('type')
# Extract last reference from return type or use entire text
if return_type_raw is not None:
return_type = parse_text(return_type_raw)
refs_gen = return_type_raw.findall('ref')
refs = [ref for ref in refs_gen]
refs = list(return_type_raw.findall('ref'))
if len(refs) > 0:
return_type = parse_text(refs[-1])
# Remove EXPORT_SYM
return_type = return_type.replace('EXPORT_SYM ', '')

# ---- Detailed description + parameter docs ----
parameters = []
parameter_items = {}
detailed_description = None

detaileddescription = node.find('detaileddescription')
detailed_description = None
parameter_items = {}
if detaileddescription is not None:
parameter_items = parse_detaileddescription(detaileddescription)
detailed_description = parse_text(detaileddescription)

# ---- Parameters ----
for param in node.findall('param'):
declname = param.find('declname')
type = param.find('type')
if declname is None or type is None: continue
type_node = param.find('type')
if declname is None or type_node is None:
continue

desc = parameter_items.get(declname.text, None)
desc_key = f"func:{name}:param:{declname.text}:description" if desc else None

parameters.append(FunctionParameter(
declname.text,
parse_text(type),
parameter_items.get(declname.text, None)
name=declname.text,
type=parse_text(type_node),
description=desc,
description_key=desc_key
))

# Extract return description from <simplesect kind="return"> in detaileddescription
# ---- Return description ----
return_description = None
if detaileddescription is not None:
return_description = detaileddescription.find('simplesect')
if return_description is not None:
return_description = parse_text(return_description)
else:
return_description = None
ret_node = detaileddescription.find('simplesect')
return_description = parse_text(ret_node) if ret_node is not None else None

brief_description = node.find('briefdescription')
# ---- Brief description ----
brief_node = node.find('briefdescription')
brief_text = parse_text(brief_node) if brief_node is not None else None

# ---- Keys ----
brief_key = f"func:{name}:brief" if brief_text else None
detailed_key = f"func:{name}:detailed" if detailed_description else None
return_key = f"func:{name}:return" if return_description else None

# ---- Append ----
functions.append(Function(
id,
name,
parameters,
return_type,
return_description,
parse_text(brief_description) if brief_description is not None else None,
detailed_description
id=id,
name=name,
parameters=parameters,
return_type=return_type,
return_description=return_description,
return_description_key=return_key,
brief_description=brief_text,
brief_description_key=brief_key,
detailed_description=detailed_description,
detailed_description_key=detailed_key
))


def parse_file(node):
global files

Expand Down
Loading
Loading