Skip to content

Commit 95fa1cf

Browse files
nicknick
authored andcommitted
version 1.1.2 - included multiple text encodings
1 parent 5d8b23b commit 95fa1cf

10 files changed

Lines changed: 806 additions & 39 deletions

File tree

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# This starter workflow is for a CMake project running on multiple platforms. There is a different starter workflow if you just want a single platform.
2-
# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-single-platform.yml
31
name: CMake on multiple platforms
42

53
on:
@@ -13,17 +11,15 @@ jobs:
1311
runs-on: ${{ matrix.os }}
1412

1513
strategy:
16-
# Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable.
14+
# Keep fail-fast off so every platform reports independently.
1715
fail-fast: false
1816

19-
# Set up a matrix to run the following 3 configurations:
20-
# 1. <Windows, Release, latest MSVC compiler toolchain on the default runner image, default generator>
21-
# 2. <Linux, Release, latest GCC compiler toolchain on the default runner image, default generator>
22-
# 3. <Linux, Release, latest Clang compiler toolchain on the default runner image, default generator>
23-
#
24-
# To add more build types (Release, Debug, RelWithDebInfo, etc.) customize the build_type list.
17+
# Matrix:
18+
# Windows — MSVC (cl) only; zlib via vendored header, iconv via Win32 API
19+
# Linux — GCC and Clang; zlib + iconv from system glibc
20+
# macOS — Apple Clang; zlib + iconv from system SDK (no extra packages)
2521
matrix:
26-
os: [ubuntu-latest, windows-latest]
22+
os: [ubuntu-latest, windows-latest, macos-latest]
2723
build_type: [Release]
2824
c_compiler: [gcc, clang, cl]
2925
include:
@@ -36,40 +32,58 @@ jobs:
3632
- os: ubuntu-latest
3733
c_compiler: clang
3834
cpp_compiler: clang++
35+
- os: macos-latest
36+
c_compiler: clang
37+
cpp_compiler: clang++
3938
exclude:
4039
- os: windows-latest
4140
c_compiler: gcc
4241
- os: windows-latest
4342
c_compiler: clang
4443
- os: ubuntu-latest
4544
c_compiler: cl
45+
- os: macos-latest
46+
c_compiler: gcc
47+
- os: macos-latest
48+
c_compiler: cl
4649

4750
steps:
4851
- uses: actions/checkout@v4
4952

53+
# ── Platform dependencies ─────────────────────────────────────────────────
54+
# Ubuntu: zlib development headers (iconv is part of glibc — no extra pkg).
55+
# macOS / Windows: zlib and iconv are available in the SDK / Win32 API; no
56+
# extra installation required.
57+
- name: Install dependencies (Ubuntu)
58+
if: runner.os == 'Linux'
59+
run: sudo apt-get update -qq && sudo apt-get install -y zlib1g-dev
60+
61+
# ── Build ─────────────────────────────────────────────────────────────────
5062
- name: Set reusable strings
51-
# Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file.
5263
id: strings
5364
shell: bash
5465
run: |
5566
echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT"
5667
5768
- name: Configure CMake
58-
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
59-
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
69+
# BUILD_PYTHON_BINDINGS=OFF: Python wheel building is handled by the
70+
# separate python-publish workflow; keeping it off avoids pybind11
71+
# dependency noise in the core C++ CI.
6072
run: >
6173
cmake -B ${{ steps.strings.outputs.build-output-dir }}
6274
-DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }}
6375
-DCMAKE_C_COMPILER=${{ matrix.c_compiler }}
6476
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
77+
-DBUILD_PYTHON_BINDINGS=OFF
6578
-S ${{ github.workspace }}
6679
6780
- name: Build
68-
# Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator).
6981
run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }}
7082

83+
# ── Test ──────────────────────────────────────────────────────────────────
7184
- name: Test
7285
working-directory: ${{ steps.strings.outputs.build-output-dir }}
73-
# Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator).
74-
# See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail
75-
run: ctest --build-config ${{ matrix.build_type }}
86+
# --output-on-failure prints the full test output when any test fails,
87+
# making it straightforward to diagnose encoding or parsing regressions
88+
# across the three platforms without inspecting runner logs manually.
89+
run: ctest --build-config ${{ matrix.build_type }} --output-on-failure

‎.github/workflows/python-publish.yml‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,37 @@ jobs:
2626
- name: Build wheels
2727
uses: pypa/cibuildwheel@v2.22.0
2828
env:
29-
# CPython 3.8 – 3.14 only; skip PyPy (not tested with this extension)
30-
CIBW_BUILD: cp38-* cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*
31-
# Skip musl/Alpine — system zlib may not be present in all musl images
29+
# CPython 3.9 – 3.14; skip PyPy (not validated with this extension).
30+
# Python 3.8 reached EOL October 2024 and is no longer built.
31+
CIBW_BUILD: cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*
32+
33+
# Skip musllinux (Alpine / musl libc) on all architectures.
34+
# Two runtime dependencies are unavailable in the default musl images:
35+
# • zlib development headers (needed at build time)
36+
# • iconv (encoding transcoding) — musl libc does not include iconv;
37+
# a separate libiconv package would be required.
38+
# Re-enable and add a CIBW_BEFORE_BUILD step if musl support is needed.
3239
CIBW_SKIP: "*-musllinux_*"
33-
# macOS: build both Intel and Apple-Silicon wheels
40+
41+
# macOS: build both Intel and Apple-Silicon wheels in a single job via
42+
# cross-compilation (no separate runner needed for arm64).
3443
CIBW_ARCHS_MACOS: x86_64 arm64
35-
# Linux: x86_64 only (add aarch64 later via QEMU if needed)
44+
45+
# Linux manylinux: x86_64 only.
46+
# Add aarch64 later via QEMU (CIBW_ARCHS_LINUX: x86_64 aarch64 +
47+
# CIBW_BEFORE_ALL_LINUX steps) if an ARM release is needed.
3648
CIBW_ARCHS_LINUX: x86_64
37-
# Windows: 64-bit only
49+
50+
# Windows: 64-bit only.
3851
CIBW_ARCHS_WINDOWS: AMD64
3952

53+
# manylinux pre-build: ensure zlib development headers are present.
54+
# iconv is part of glibc in all manylinux images — no extra install.
55+
CIBW_BEFORE_BUILD_LINUX: yum install -y zlib-devel || apt-get install -y zlib1g-dev || true
56+
57+
# macOS pre-build: zlib and iconv are in the Xcode SDK; no extra step.
58+
# Windows: zlib is vendored (vendor/zlib/zlib.h); iconv uses Win32 API.
59+
4060
- uses: actions/upload-artifact@v4
4161
with:
4262
name: cibw-wheels-${{ matrix.os }}
@@ -54,10 +74,11 @@ jobs:
5474
with:
5575
python-version: "3.x"
5676

77+
- name: Install build frontend
78+
run: python -m pip install build
79+
5780
- name: Build sdist
58-
run: |
59-
python -m pip install build
60-
python -m build --sdist
81+
run: python -m build --sdist
6182

6283
- uses: actions/upload-artifact@v4
6384
with:
@@ -71,7 +92,7 @@ jobs:
7192
needs: [build-wheels, build-sdist]
7293

7394
permissions:
74-
# Required for OIDC Trusted Publishing — no API token needed
95+
# Required for OIDC Trusted Publishing — no API token needed.
7596
id-token: write
7697

7798
environment:

‎.gitignore‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ BUILDING.md
99
/build
1010
.pypirc
1111
/build_mingw64
12-
/build_msvc
12+
/build_msvc
13+
/dist
14+
/out

‎CMakeLists.txt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
cmake_minimum_required(VERSION 3.15)
2-
project(docx_comment_parser VERSION 1.1.1 LANGUAGES CXX)
2+
project(docx_comment_parser VERSION 1.1.2 LANGUAGES CXX)
33

44
# ─── Policy: normalise install() DESTINATION paths (fixes CMP0177 warning) ───
55
if(POLICY CMP0177)
@@ -32,6 +32,14 @@ if(NOT MSVC)
3232
find_package(ZLIB REQUIRED)
3333
endif()
3434

35+
# ─── iconv (encoding transcoding) ─────────────────────────────────────────────
36+
# Windows uses the native MultiByteToWideChar/WideCharToMultiByte APIs (no
37+
# extra library). On Linux/macOS iconv is typically part of the C library; on
38+
# musl or standalone environments a separate libiconv may be needed.
39+
if(NOT WIN32)
40+
find_package(Iconv QUIET) # sets Iconv::Iconv; harmless no-op when built-in
41+
endif()
42+
3543
# ─── Common compile options helper ────────────────────────────────────────────
3644
# Applied to every target that compiles the library sources.
3745
function(docx_apply_compile_options target)
@@ -61,6 +69,9 @@ function(docx_link_platform_libs target)
6169
target_link_libraries(${target} PRIVATE
6270
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:ZLIB::ZLIB>
6371
)
72+
if(NOT WIN32 AND Iconv_FOUND AND NOT Iconv_IS_BUILT_IN)
73+
target_link_libraries(${target} PRIVATE Iconv::Iconv)
74+
endif()
6475
if(MINGW AND _has_pthread_h)
6576
target_link_libraries(${target} PRIVATE -lpthread)
6677
endif()

‎README.md‎

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A C++17 shared library that extracts every piece of comment metadata from `.docx
44

55
[![Tests](https://img.shields.io/badge/tests-38%20passing-brightgreen)](#testing)
66
[![C++17](https://img.shields.io/badge/C%2B%2B-17-blue)](#building-the-shared-library)
7-
[![Python ≥ 3.8](https://img.shields.io/badge/python-%E2%89%A53.8-blue)](#python-quick-start)
7+
[![Python ≥ 3.9](https://img.shields.io/badge/python-%E2%89%A53.8-blue)](#python-quick-start)
88
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](#license)
99

1010
---
@@ -618,17 +618,39 @@ Expected output:
618618
Test fixture: /tmp/test_docx_parser_fixture.docx
619619
620620
=== test_basic_parsing ===
621+
621622
=== test_threading ===
623+
622624
=== test_done_flag ===
625+
623626
=== test_anchor_text ===
627+
624628
=== test_by_author ===
629+
625630
=== test_stats ===
631+
626632
=== test_root_comments ===
633+
627634
=== test_batch_parser ===
635+
628636
=== test_missing_file ===
629637
638+
=== test_encoding_utf8_bom ===
639+
640+
=== test_encoding_utf16le ===
641+
642+
=== test_encoding_utf16be ===
643+
644+
=== test_encoding_utf32le ===
645+
646+
=== test_encoding_windows1252 ===
647+
648+
=== test_encoding_iso8859_1 ===
649+
650+
=== test_encoding_numeric_entities ===
651+
630652
──────────────────────────────
631-
Results: 38 passed, 0 failed
653+
Results: 66 passed, 0 failed
632654
```
633655

634656
The test binary exits with code `0` on full pass, `1` on any failure.
@@ -637,6 +659,41 @@ The test binary exits with code `0` on full pass, `1` on any failure.
637659

638660
## Changelog
639661

662+
### v1.1.2 — Added multiple enconding support
663+
664+
Included multiple text enconding support for a wide range of encondings. Updated unit tests for the new text enconding functionality.
665+
666+
#### `src/xml_parser.cpp` — Added a complete encoding transcoding layer before the XML parser:
667+
668+
`extract_xml_encoding_decl()` — scans the XML prolog for encoding="..."
669+
670+
`detect_encoding()` — BOM detection (UTF-8/16/32 LE/BE) takes precedence, falls back to the XML declaration
671+
672+
`utf16_to_utf8()` / `utf32_to_utf8()` — built-in converters (no platform dependency) with correct surrogate-pair handling
673+
674+
**Windows path:** `win_mbcs_to_utf8()` via `MultiByteToWideChar` + `WideCharToMultiByte`; maps 60+ encoding names to Windows codepage numbers (all Windows-125x, ISO-8859-1..16, Asian, Cyrillic, Thai, OEM codepages)
675+
676+
**Linux/macOS path:** `iconv_convert()` via `iconv(3)` with the same name alias table; handles `E2BIG`/`EILSEQ`/`EINVAL` gracefully
677+
`transcode_to_utf8()` — public entry point, called at the start of `sax_parse()` so all parsing paths (DOM and SAX) go through it automatically
678+
679+
#### `include/xml_parser.h` — Exposed transcode_to_utf8() as a public API with full docstring.
680+
681+
#### `CMakeLists.txt` — Added `find_package(Iconv QUIET)` for non-Windows targets; links `Iconv::Iconv` only when it's a separate library (not built into libc).
682+
683+
#### `tests/test_docx_parser.cpp` — Added 7 encoding tests (66 total, all green):
684+
685+
`test_encoding_utf8_bom` — UTF-8 BOM is silently stripped
686+
687+
`test_encoding_utf16le` / `test_encoding_utf16be` — BOM-detected UTF-16
688+
689+
`test_encoding_utf32le` — BOM-detected UTF-32
690+
691+
`test_encoding_windows1252` — `encoding="windows-1252"` with ç, é, ä in content
692+
693+
`test_encoding_iso8859_1` — `encoding="ISO-8859-1"` with é, ñ
694+
695+
`test_encoding_numeric_entities` — `&#x4E2D;` (Chinese) and `&#233;` (é) references
696+
640697
### v1.1.0 — Inflate fix and zero-dependency MSVC support
641698

642699
**Public API:** unchanged. Existing code does not need modification.

‎include/xml_parser.h‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,25 @@ std::string node_text(const XmlNode& node);
9696
/** Truncate a string to max_bytes (at a UTF-8 boundary). */
9797
std::string truncate_utf8(const std::string& s, std::size_t max_bytes);
9898

99+
/**
100+
* Convert an arbitrary-encoded XML byte stream to UTF-8.
101+
*
102+
* Detects encoding via BOM (UTF-8/16/32 LE/BE) and/or the XML declaration's
103+
* encoding="..." attribute. Supported encodings include:
104+
* - Unicode: UTF-8, UTF-16 LE/BE, UTF-32 LE/BE (with or without BOM)
105+
* - European: ISO-8859-1..16, Windows-125x, KOI8-R/U, IBM OEM codepages
106+
* - Asian: Shift-JIS, GBK/GB2312, GB18030, Big5, EUC-JP/KR/CN,
107+
* ISO-2022-JP/KR/CN
108+
* - Other: TIS-620 (Thai), US-ASCII, and any encoding supported by the
109+
* platform's iconv (Linux/macOS) or MultiByteToWideChar (Windows).
110+
*
111+
* Returns the UTF-8 string. If the input is already valid UTF-8 (the common
112+
* case for OOXML), a copy is returned without any conversion overhead beyond
113+
* a BOM strip if needed.
114+
*
115+
* A trailing NUL byte appended by callers for parser safety is silently
116+
* stripped before conversion.
117+
*/
118+
std::string transcode_to_utf8(const char* data, std::size_t len);
119+
99120
} // namespace docx

‎pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ build-backend = "scikit_build_core.build"
77

88
[project]
99
name = "docx-comment-parser"
10-
version = "1.1.1"
10+
version = "1.1.2"
1111
description = "Fast C++ library for extracting comment metadata from .docx files"
1212
readme = "README.md"
13-
requires-python = ">=3.8"
13+
requires-python = ">=3.9"
1414
authors = [{name = "nick-developer"}]
1515
classifiers = [
1616
"Programming Language :: Python :: 3",

‎setup.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def _is_mingw() -> bool:
123123

124124
setup(
125125
name="docx-comment-parser",
126-
version="1.1.1",
126+
version="1.1.2",
127127
author="nick-developer",
128128
description="Fast C++ library for extracting comment metadata from .docx files",
129129
long_description=(
@@ -132,7 +132,7 @@ def _is_mingw() -> bool:
132132
else ""
133133
),
134134
ext_modules=[ext],
135-
python_requires=">=3.8",
135+
python_requires=">=3.9",
136136
classifiers=[
137137
"Programming Language :: Python :: 3",
138138
"Programming Language :: C++",

0 commit comments

Comments
 (0)