mirror of
https://github.com/HKUDS/CLI-Anything.git
synced 2026-08-28 23:27:04 +08:00
fix(libreoffice): address import review comments
This commit is contained in:
@@ -156,7 +156,11 @@ def _apply_metadata(project: Dict[str, Any], meta_xml: str, source_path: str) ->
|
||||
if not meta_xml:
|
||||
return
|
||||
|
||||
root = ET.fromstring(meta_xml)
|
||||
try:
|
||||
root = ET.fromstring(meta_xml)
|
||||
except ET.ParseError as e:
|
||||
raise ValueError(f"Invalid ODF meta.xml in: {source_path}") from e
|
||||
|
||||
mappings = {
|
||||
"title": ("dc", "title"),
|
||||
"author": ("dc", "creator"),
|
||||
@@ -295,7 +299,7 @@ def _parse_calc_row(row_elem: ET.Element, row_index: int) -> Dict[str, Dict[str,
|
||||
|
||||
def _cell_data(cell_elem: ET.Element) -> Optional[Dict[str, Any]]:
|
||||
value_type = _attr(cell_elem, "office", "value-type") or "string"
|
||||
formula = _attr(cell_elem, "table", "formula")
|
||||
formula = _normalize_formula(_attr(cell_elem, "table", "formula"))
|
||||
text = _text_content(cell_elem)
|
||||
numeric_value = _attr(cell_elem, "office", "value")
|
||||
|
||||
@@ -322,6 +326,16 @@ def _cell_data(cell_elem: ET.Element) -> Optional[Dict[str, Any]]:
|
||||
return data
|
||||
|
||||
|
||||
def _normalize_formula(formula: Optional[str]) -> Optional[str]:
|
||||
"""Strip ODF formula namespace prefixes before storing project state."""
|
||||
if formula is None:
|
||||
return None
|
||||
for prefix in ("of:", "oooc:"):
|
||||
if formula.startswith(prefix):
|
||||
return formula[len(prefix):]
|
||||
return formula
|
||||
|
||||
|
||||
def _parse_impress_content(root: ET.Element) -> List[Dict[str, Any]]:
|
||||
body = root.find(_q("office", "body"))
|
||||
presentation = body.find(_q("office", "presentation")) if body is not None else None
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
| File | Test Classes | Test Count | Focus |
|
||||
|------|-------------|------------|-------|
|
||||
| `test_core.py` | 7 | 97 | Unit tests for document, import, Writer, Calc, Impress, styles, session |
|
||||
| `test_core.py` | 7 | 99 | Unit tests for document, import, Writer, Calc, Impress, styles, session |
|
||||
| `test_full_e2e.py` | 17 | 73 | E2E workflows: ODF ZIP structure, XML validity, import, export formats, CLI subprocess |
|
||||
| **Total** | **24** | **170** | |
|
||||
| **Total** | **24** | **172** | |
|
||||
|
||||
## Unit Tests (`test_core.py`)
|
||||
|
||||
@@ -23,14 +23,16 @@ All unit tests use synthetic/in-memory data only. No LibreOffice installation re
|
||||
- List available profiles
|
||||
- Metadata populated on creation (title, created date)
|
||||
|
||||
### TestImport (7 tests)
|
||||
### TestImport (9 tests)
|
||||
- List supported import formats, including ODF and Microsoft Office extensions
|
||||
- Import generated ODT into Writer project content
|
||||
- Import generated ODS into Calc sheets and cells
|
||||
- Normalize imported Calc formulas before re-export
|
||||
- Import generated ODP into Impress slides
|
||||
- Route DOCX import through LibreOffice conversion without requiring LibreOffice in unit tests
|
||||
- Reject unsupported import formats
|
||||
- Reject invalid ODF files with a clean error
|
||||
- Reject malformed ODF meta.xml with a clean error
|
||||
|
||||
### TestWriter (18 tests)
|
||||
- Add paragraph with default and custom style
|
||||
@@ -171,6 +173,6 @@ E2E tests produce real ODF files (ODT/ODS/ODP) and validate ZIP structure, XML c
|
||||
## Test Results
|
||||
|
||||
```
|
||||
test_core.py: 97 passed in 0.15s
|
||||
test_core.py: 99 passed in 0.15s
|
||||
test_full_e2e.py: 73 passed in 58.33s
|
||||
```
|
||||
|
||||
@@ -8,6 +8,7 @@ import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
import zipfile
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
@@ -35,6 +36,7 @@ from cli_anything.libreoffice.core.styles import (
|
||||
from cli_anything.libreoffice.core.session import Session
|
||||
from cli_anything.libreoffice.core.export import to_odt, to_ods, to_odp
|
||||
from cli_anything.libreoffice.core import importer as importer_mod
|
||||
from cli_anything.libreoffice.utils.odf_utils import parse_odf
|
||||
|
||||
|
||||
# ── Document Tests ───────────────────────────────────────────────
|
||||
@@ -186,6 +188,25 @@ class TestImport:
|
||||
assert imported["sheets"][0]["cells"]["B1"]["value"] == 42.0
|
||||
assert imported["sheets"][0]["cells"]["B1"]["type"] == "float"
|
||||
|
||||
def test_import_calc_formula_normalizes_odf_prefix(self):
|
||||
proj = create_document(doc_type="calc", name="formula_calc")
|
||||
set_cell(proj, "A1", "1", cell_type="float")
|
||||
set_cell(proj, "A2", "2", cell_type="float")
|
||||
set_cell(proj, "A3", "0", cell_type="float", formula="=A1+A2")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
source = os.path.join(tmp, "formula.ods")
|
||||
roundtrip = os.path.join(tmp, "roundtrip.ods")
|
||||
to_ods(proj, source)
|
||||
imported = importer_mod.import_document(source)
|
||||
to_ods(imported, roundtrip)
|
||||
content_xml = parse_odf(roundtrip)["content_xml"]
|
||||
|
||||
formula = imported["sheets"][0]["cells"]["A3"]["formula"]
|
||||
assert formula == "=A1+A2"
|
||||
assert 'table:formula="of:=A1+A2"' in content_xml
|
||||
assert "of:of:=" not in content_xml
|
||||
|
||||
def test_import_impress_odp(self):
|
||||
proj = create_document(doc_type="impress", name="import_impress")
|
||||
add_slide(proj, title="Intro", content="Welcome")
|
||||
@@ -246,6 +267,25 @@ class TestImport:
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
def test_reject_malformed_odf_meta_xml(self):
|
||||
proj = create_document(doc_type="writer")
|
||||
add_paragraph(proj, text="body")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
source = os.path.join(tmp, "source.odt")
|
||||
malformed = os.path.join(tmp, "malformed.odt")
|
||||
to_odt(proj, source)
|
||||
|
||||
with zipfile.ZipFile(source, "r") as zin, zipfile.ZipFile(malformed, "w") as zout:
|
||||
for info in zin.infolist():
|
||||
data = zin.read(info.filename)
|
||||
if info.filename == "meta.xml":
|
||||
data = b"<broken>"
|
||||
zout.writestr(info, data)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid ODF meta.xml"):
|
||||
importer_mod.import_document(malformed)
|
||||
|
||||
|
||||
# ── Writer Tests ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@@ -112,20 +113,26 @@ def convert(
|
||||
tempfile.TemporaryDirectory(prefix="lo-runtime-") as runtime_dir, \
|
||||
tempfile.TemporaryDirectory(prefix="lo-config-") as config_dir, \
|
||||
tempfile.TemporaryDirectory(prefix="lo-cache-") as cache_dir:
|
||||
os.chmod(runtime_dir, 0o700)
|
||||
env = os.environ.copy()
|
||||
env.update({
|
||||
"XDG_RUNTIME_DIR": runtime_dir,
|
||||
"XDG_CONFIG_HOME": config_dir,
|
||||
"XDG_CACHE_HOME": cache_dir,
|
||||
})
|
||||
if os.name == "posix":
|
||||
try:
|
||||
os.chmod(runtime_dir, 0o700)
|
||||
except OSError:
|
||||
pass
|
||||
env.update({
|
||||
"XDG_RUNTIME_DIR": runtime_dir,
|
||||
"XDG_CONFIG_HOME": config_dir,
|
||||
"XDG_CACHE_HOME": cache_dir,
|
||||
})
|
||||
|
||||
profile_uri = Path(profile_dir).resolve().as_uri()
|
||||
|
||||
cmd = [
|
||||
lo,
|
||||
"--headless",
|
||||
"--nologo",
|
||||
"--nofirststartwizard",
|
||||
f"-env:UserInstallation=file://{profile_dir}",
|
||||
f"-env:UserInstallation={profile_uri}",
|
||||
"--convert-to", output_format,
|
||||
"--outdir", output_dir,
|
||||
input_path,
|
||||
|
||||
Reference in New Issue
Block a user