Skip to content
Merged
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
92 changes: 79 additions & 13 deletions doclang/_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@
<Default Extension="jpg" ContentType="image/jpeg"/>
<Default Extension="jpeg" ContentType="image/jpeg"/>
<Default Extension="webp" ContentType="image/webp"/>
<Default Extension="mp3" ContentType="audio/mpeg"/>
<Default Extension="m4a" ContentType="audio/mp4"/>
<Default Extension="aac" ContentType="audio/aac"/>
<Default Extension="ogg" ContentType="audio/ogg"/>
<Default Extension="opus" ContentType="audio/ogg"/>
<Default Extension="wav" ContentType="audio/wav"/>
<Default Extension="flac" ContentType="audio/flac"/>
<Default Extension="mp4" ContentType="video/mp4"/>
<Default Extension="webm" ContentType="video/webm"/>
<Default Extension="mkv" ContentType="video/x-matroska"/>
<Default Extension="mov" ContentType="video/quicktime"/>
<Override PartName="/document.xml" ContentType="application/vnd.doclang.document+xml"/>
</Types>
"""
Expand All @@ -37,6 +48,9 @@
Mapping[int, Union[str, Path]],
]

# Whole-track media (audio/, video/) uses the same 1-based positional model as pages/.
MediaInput = PagesInput

AssetsInput = Union[
str,
Path,
Expand Down Expand Up @@ -96,25 +110,71 @@ def _place_document(stage: Path, document: Path) -> None:
shutil.copy2(document, stage / "document.xml")


def _place_pages(stage: Path, pages: PagesInput) -> None:
pages_dir = stage / "pages"
if isinstance(pages, Mapping):
for page_number, source in pages.items():
if not isinstance(page_number, int) or page_number < 1:
raise PackagingError(f"Page numbers must be positive integers, got {page_number!r}")
def _place_indexed(
stage: Path,
spec: PagesInput,
*,
subdir: str,
file_label: str,
dir_label: str,
number_label: str,
) -> None:
"""Place 1-based positionally-indexed files (``{N}.{ext}``) under ``stage/subdir``.

Shared by ``pages/`` (page images) and ``audio/`` / ``video/`` (whole-track media),
which all key a file to an integer position in the document.
"""
target_dir = stage / subdir
if isinstance(spec, Mapping):
for number, source in spec.items():
if not isinstance(number, int) or number < 1:
raise PackagingError(f"{number_label} must be positive integers, got {number!r}")
source_path = Path(source)
destination = pages_dir / f"{page_number}{source_path.suffix}"
_copy_file(source_path, destination, label="Page file")
destination = target_dir / f"{number}{source_path.suffix}"
_copy_file(source_path, destination, label=file_label)
return

if isinstance(pages, str | Path):
_copy_tree_into(Path(pages), pages_dir, label="Pages directory")
if isinstance(spec, str | Path):
_copy_tree_into(Path(spec), target_dir, label=dir_label)
return

for index, source in enumerate(pages, start=1):
for index, source in enumerate(spec, start=1):
source_path = Path(source)
destination = pages_dir / f"{index}{source_path.suffix}"
_copy_file(source_path, destination, label="Page file")
destination = target_dir / f"{index}{source_path.suffix}"
_copy_file(source_path, destination, label=file_label)


def _place_pages(stage: Path, pages: PagesInput) -> None:
_place_indexed(
stage,
pages,
subdir="pages",
file_label="Page file",
dir_label="Pages directory",
number_label="Page numbers",
)


def _place_audio(stage: Path, audio: MediaInput) -> None:
_place_indexed(
stage,
audio,
subdir="audio",
file_label="Audio file",
dir_label="Audio directory",
number_label="Audio track numbers",
)


def _place_video(stage: Path, video: MediaInput) -> None:
_place_indexed(
stage,
video,
subdir="video",
file_label="Video file",
dir_label="Video directory",
number_label="Video track numbers",
)


def _place_assets(stage: Path, assets: AssetsInput) -> None:
Expand Down Expand Up @@ -164,6 +224,8 @@ def _pack(
output: Union[str, Path, None] = None,
pages: PagesInput | None = None,
assets: AssetsInput | None = None,
audio: MediaInput | None = None,
video: MediaInput | None = None,
validate: bool = False,
) -> Path:
document_path = Path(document)
Expand All @@ -176,6 +238,10 @@ def _pack(
_place_pages(stage, pages)
if assets is not None:
_place_assets(stage, assets)
if audio is not None:
_place_audio(stage, audio)
if video is not None:
_place_video(stage, video)
_write_opc_metadata(stage)

if validate:
Expand Down
50 changes: 48 additions & 2 deletions doclang/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,49 @@ def pack(
file_okay=False,
dir_okay=True,
),
audio_dir: Path | None = typer.Option(
None,
"--audio-dir",
help="Directory of whole-track audio files (2.mp3, 4.ogg, …)",
exists=True,
file_okay=False,
dir_okay=True,
),
audio_files: list[Path] | None = typer.Option(
None,
"--audio",
help="Whole-track audio file; repeat to add tracks in order (renumbered as 1.ext, 2.ext, …)",
exists=True,
file_okay=True,
dir_okay=False,
),
video_dir: Path | None = typer.Option(
None,
"--video-dir",
help="Directory of whole-track video files (3.mp4, 4.mkv, …)",
exists=True,
file_okay=False,
dir_okay=True,
),
video_files: list[Path] | None = typer.Option(
None,
"--video",
help="Whole-track video file; repeat to add tracks in order (renumbered as 1.ext, 2.ext, …)",
exists=True,
file_okay=True,
dir_okay=False,
),
validate_before_pack: bool = typer.Option(False, "--validate", help="Validate document before packing"),
quiet: bool = typer.Option(False, "--quiet", "-q", help="Quiet mode (exit code only)"),
):
"""
Pack a DocLang markup file and optional media into a .dclx archive.

DOCUMENT is copied to document.xml. Optional page images (--pages, --page)
are placed under pages/. Optional payload files (--assets, --asset) are
placed under assets/ for URIs referenced in the markup. OPC metadata
are placed under pages/. Optional whole-track audio/video (--audio-dir,
--audio, --video-dir, --video) are placed under audio/ and video/ as
{N}.{ext} for the Nth <track>. Optional payload files (--assets, --asset)
are placed under assets/ for URIs referenced in the markup. OPC metadata
([Content_Types].xml, _rels/.rels) is generated automatically.

By default, writes <document>.dclx next to the input file.
Expand All @@ -219,6 +253,7 @@ def pack(
doclang pack markup.dclg -o report.dclx --pages screenshots/
doclang pack markup.dclg --page a.png --page b.png
doclang pack markup.dclg --asset chart.svg=exports/diagram.svg
doclang pack markup.dclg --audio-dir recordings/ --video-dir recordings/
doclang pack markup.dclg --assets payload/ --validate
"""
if pages_dir is not None and page_files:
Expand All @@ -227,6 +262,12 @@ def pack(
if assets_dir is not None and asset_mappings:
typer.echo("Error: --assets and --asset are mutually exclusive", err=True)
raise typer.Exit(1)
if audio_dir is not None and audio_files:
typer.echo("Error: --audio-dir and --audio are mutually exclusive", err=True)
raise typer.Exit(1)
if video_dir is not None and video_files:
typer.echo("Error: --video-dir and --video are mutually exclusive", err=True)
raise typer.Exit(1)

output_path = output or document.with_suffix(".dclx")

Expand All @@ -246,12 +287,17 @@ def pack(
else:
assets = None

audio: Path | list[Path] | None = audio_dir if audio_dir is not None else (audio_files or None)
video: Path | list[Path] | None = video_dir if video_dir is not None else (video_files or None)

try:
created = pack_document(
document,
output=output_path,
pages=pages,
assets=assets,
audio=audio,
video=video,
validate=validate_before_pack,
)
except ValidationError as exc:
Expand Down
Loading
Loading