marktip 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ name: Build distributions
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+ tags: ["v*"]
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ sdist:
15
+ name: Build source distribution
16
+ runs-on: ubuntu-latest
17
+
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+ with:
21
+ persist-credentials: false
22
+
23
+ - uses: actions/setup-python@v6
24
+ with:
25
+ python-version: "3.x"
26
+
27
+ - name: Install build tools
28
+ run: python -m pip install build twine
29
+
30
+ - name: Build sdist
31
+ run: python -m build --sdist --outdir dist
32
+
33
+ - name: Check sdist
34
+ run: python -m twine check dist/*
35
+
36
+ - uses: actions/upload-artifact@v6
37
+ with:
38
+ name: marktip-sdist
39
+ path: dist/*.tar.gz
40
+
41
+ wheels:
42
+ name: Build wheels on ${{ matrix.os }}
43
+ runs-on: ${{ matrix.os }}
44
+ strategy:
45
+ fail-fast: false
46
+ matrix:
47
+ os: [ubuntu-latest, windows-latest, macos-15-intel, macos-latest]
48
+
49
+ steps:
50
+ - uses: actions/checkout@v6
51
+ with:
52
+ persist-credentials: false
53
+
54
+ - uses: actions/setup-python@v6
55
+
56
+ - name: Install cibuildwheel
57
+ run: python -m pip install cibuildwheel==4.1.0
58
+
59
+ - name: Build wheels
60
+ run: python -m cibuildwheel --output-dir wheelhouse
61
+
62
+ - uses: actions/upload-artifact@v6
63
+ with:
64
+ name: marktip-wheels-${{ matrix.os }}
65
+ path: wheelhouse/*.whl
@@ -0,0 +1,10 @@
1
+ build/
2
+ dist/
3
+ *.egg-info/
4
+ *.whl
5
+ __pycache__/
6
+ *.py[cod]
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .venv/
@@ -0,0 +1,40 @@
1
+ cmake_minimum_required(VERSION 3.15)
2
+
3
+ project(marktip LANGUAGES C CXX)
4
+
5
+ set(CMAKE_CXX_STANDARD 17)
6
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
7
+ set(CMAKE_CXX_EXTENSIONS OFF)
8
+
9
+ if(NOT DEFINED SKBUILD_PROJECT_VERSION)
10
+ set(SKBUILD_PROJECT_VERSION "0.1.0")
11
+ endif()
12
+
13
+ set(PYBIND11_FINDPYTHON ON)
14
+ find_package(pybind11 CONFIG REQUIRED)
15
+
16
+ add_library(md4c STATIC
17
+ third_party/md4c/src/md4c.c
18
+ third_party/md4c/src/entity.c
19
+ )
20
+ target_include_directories(md4c PUBLIC third_party/md4c/src)
21
+ target_compile_definitions(md4c PRIVATE MD4C_USE_UTF8)
22
+ set_target_properties(md4c PROPERTIES POSITION_INDEPENDENT_CODE ON)
23
+
24
+ pybind11_add_module(_core
25
+ src/ast.cpp
26
+ src/marktip.cpp
27
+ src/parser.cpp
28
+ src/serializer.cpp
29
+ )
30
+ target_link_libraries(_core PRIVATE md4c)
31
+ target_include_directories(_core PRIVATE third_party/md4c/src)
32
+ target_compile_definitions(_core PRIVATE MARKTIP_VERSION="${SKBUILD_PROJECT_VERSION}")
33
+
34
+ if(MSVC)
35
+ target_compile_options(_core PRIVATE /O2)
36
+ else()
37
+ target_compile_options(_core PRIVATE -O3 -Wall -Wextra -Wpedantic)
38
+ endif()
39
+
40
+ install(TARGETS _core LIBRARY DESTINATION marktip)
marktip-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Minjae Kyung
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
marktip-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: marktip
3
+ Version: 0.1.0
4
+ Summary: Fast C++/MD4C Markdown parser and canonical Markdown serializer for Tiptap-style JSON
5
+ Keywords: markdown,tiptap,md4c,parser
6
+ Author: Saneaven (Minjae Kyung)
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ License-File: third_party/md4c/LICENSE.md
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: C++
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Text Processing :: Markup :: Markdown
24
+ Requires-Python: >=3.9
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=8; extra == "test"
27
+ Description-Content-Type: text/markdown
28
+
29
+ # marktip
30
+
31
+ Fast C++/MD4C Markdown conversion for Tiptap-style JSON.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ python -m pip install marktip
37
+ ```
38
+
39
+ Release wheels are built for common CPython versions on Linux, macOS, and
40
+ Windows. If a wheel is not available for a platform, pip can build from the
41
+ source distribution with a C++17 compiler and standard Python build tooling.
42
+
43
+ ## Usage
44
+
45
+ ```python
46
+ import marktip as tm
47
+
48
+ doc = tm.from_markdown("# Hello")
49
+ ast = doc.to_dict()
50
+ markdown = doc.to_markdown()
51
+
52
+ doc = tm.from_dict(ast)
53
+ ```
54
+
55
+ The first version targets GFM core syntax and canonical Markdown output rather
56
+ than byte-identical source preservation.
57
+
58
+ ## Development
59
+
60
+ ```bash
61
+ python -m pip install .[test]
62
+ python -m pytest
63
+ ```
64
+
65
+ For a direct local CMake build:
66
+
67
+ ```bash
68
+ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
69
+ -Dpybind11_DIR="$(python -m pybind11 --cmakedir)"
70
+ cmake --build build
71
+ PYTHONPATH=python python -m pytest
72
+ PYTHONPATH=python python scripts/benchmark.py
73
+ ```
@@ -0,0 +1,45 @@
1
+ # marktip
2
+
3
+ Fast C++/MD4C Markdown conversion for Tiptap-style JSON.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ python -m pip install marktip
9
+ ```
10
+
11
+ Release wheels are built for common CPython versions on Linux, macOS, and
12
+ Windows. If a wheel is not available for a platform, pip can build from the
13
+ source distribution with a C++17 compiler and standard Python build tooling.
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ import marktip as tm
19
+
20
+ doc = tm.from_markdown("# Hello")
21
+ ast = doc.to_dict()
22
+ markdown = doc.to_markdown()
23
+
24
+ doc = tm.from_dict(ast)
25
+ ```
26
+
27
+ The first version targets GFM core syntax and canonical Markdown output rather
28
+ than byte-identical source preservation.
29
+
30
+ ## Development
31
+
32
+ ```bash
33
+ python -m pip install .[test]
34
+ python -m pytest
35
+ ```
36
+
37
+ For a direct local CMake build:
38
+
39
+ ```bash
40
+ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
41
+ -Dpybind11_DIR="$(python -m pybind11 --cmakedir)"
42
+ cmake --build build
43
+ PYTHONPATH=python python -m pytest
44
+ PYTHONPATH=python python scripts/benchmark.py
45
+ ```
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["scikit-build-core>=0.10", "pybind11>=2.12"]
3
+ build-backend = "scikit_build_core.build"
4
+
5
+ [project]
6
+ name = "marktip"
7
+ version = "0.1.0"
8
+ description = "Fast C++/MD4C Markdown parser and canonical Markdown serializer for Tiptap-style JSON"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE", "third_party/md4c/LICENSE.md"]
13
+ authors = [{ name = "Saneaven (Minjae Kyung)" }]
14
+ keywords = ["markdown", "tiptap", "md4c", "parser"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: MacOS",
19
+ "Operating System :: Microsoft :: Windows",
20
+ "Operating System :: POSIX :: Linux",
21
+ "Programming Language :: C++",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: Text Processing :: Markup :: Markdown",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ test = ["pytest>=8"]
34
+
35
+ [tool.scikit-build]
36
+ cmake.build-type = "Release"
37
+ wheel.packages = ["python/marktip"]
38
+ sdist.include = [
39
+ "third_party/md4c/src/md4c.c",
40
+ "third_party/md4c/src/md4c.h",
41
+ "third_party/md4c/src/entity.c",
42
+ "third_party/md4c/src/entity.h",
43
+ "third_party/md4c/LICENSE.md",
44
+ ]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+
49
+ [tool.cibuildwheel]
50
+ build = "cp39-* cp310-* cp311-* cp312-* cp313-* cp314-*"
51
+ skip = ["*-win32", "*-manylinux_i686", "*-musllinux*"]
52
+ test-extras = ["test"]
53
+ test-command = "pytest {project}/tests"
@@ -0,0 +1,27 @@
1
+ """Fast Markdown conversion for Tiptap-style JSON."""
2
+
3
+ try:
4
+ from ._core import Document, __version__, from_dict, from_markdown
5
+ except ImportError: # pragma: no cover - development-only CMake build fallback
6
+ import importlib.util
7
+ import pathlib
8
+ import sys
9
+
10
+ _root = pathlib.Path(__file__).resolve().parents[2]
11
+ _candidates = sorted(_root.glob("build/**/_core*.so")) + sorted(_root.glob("build/**/_core*.pyd"))
12
+ if not _candidates:
13
+ raise
14
+
15
+ _spec = importlib.util.spec_from_file_location("marktip._core", _candidates[0])
16
+ if _spec is None or _spec.loader is None:
17
+ raise
18
+ _module = importlib.util.module_from_spec(_spec)
19
+ sys.modules[_spec.name] = _module
20
+ _spec.loader.exec_module(_module)
21
+
22
+ __version__ = _module.__version__
23
+ Document = _module.Document
24
+ from_dict = _module.from_dict
25
+ from_markdown = _module.from_markdown
26
+
27
+ __all__ = ["__version__", "Document", "from_markdown", "from_dict"]
@@ -0,0 +1,131 @@
1
+ """Local performance smoke test with a large mixed Markdown document.
2
+
3
+ Run after building/installing the package:
4
+
5
+ PYTHONPATH=python python scripts/benchmark.py
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import concurrent.futures
12
+ import time
13
+ import tracemalloc
14
+
15
+ import marktip as tm
16
+
17
+
18
+ LOREM = (
19
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. "
20
+ "Praesent vitae augue sed neque efficitur vulputate. "
21
+ "Integer non sem at orci pretium aliquet. "
22
+ "Suspendisse potenti, sed fermentum risus sagittis in. "
23
+ "Curabitur blandit, nibh in dignissim gravida, sapien risus feugiat arcu, "
24
+ "vel pretium erat sem vitae erat. "
25
+ )
26
+
27
+
28
+ def lorem_words(repetitions: int) -> str:
29
+ return LOREM * repetitions
30
+
31
+
32
+ def make_fixture(sections: int = 1_200, lorem_repetitions: int = 4) -> str:
33
+ chunks: list[str] = ["# Synthetic Lorem Markdown Corpus\n\n"]
34
+ long_lorem = lorem_words(lorem_repetitions)
35
+
36
+ for index in range(sections):
37
+ checked = "x" if index % 2 == 0 else " "
38
+ chunks.append(
39
+ f"## Section {index}: Lorem Throughput\n\n"
40
+ f"{long_lorem}"
41
+ f"This paragraph mixes **bold emphasis {index}**, *italic phrasing*, "
42
+ f"~~deleted context~~, `inline_code_{index}`, "
43
+ f"[reference link](https://example.com/docs/{index}), and <span data-i=\"{index}\">inline HTML</span>. \n"
44
+ f"It also keeps a hard break before continuing with more prose. {long_lorem}\n\n"
45
+ f"> {long_lorem}"
46
+ f"> Nested quotation with **strong text**, [a citation](https://example.com/cite/{index}), "
47
+ f"> and enough lorem ipsum to look like a real imported document.\n\n"
48
+ f"- [{checked}] Review generated section {index}\n"
49
+ f"- [ ] Normalize serializer output for section {index}\n"
50
+ f"- [ ] Compare parse and serialization latency\n\n"
51
+ f"1. Ordered item alpha for section {index}\n"
52
+ f"2. Ordered item beta with `code` and **marks**\n"
53
+ f" - Nested bullet carries {long_lorem[:180]}\n\n"
54
+ "| Metric | Value | Notes |\n"
55
+ "| :--- | ---: | :--- |\n"
56
+ f"| section | {index} | lorem table cell with **markdown** and pipes escaped later |\n"
57
+ f"| bytes | {len(long_lorem)} | [source](https://example.com/bench/{index}) |\n\n"
58
+ "```cpp\n"
59
+ f"// synthetic code block for section {index}\n"
60
+ "int accumulate_markdown_nodes(int base) {\n"
61
+ " int total = base;\n"
62
+ " for (int i = 0; i < 32; ++i) {\n"
63
+ " total += i * 3;\n"
64
+ " }\n"
65
+ " return total;\n"
66
+ "}\n"
67
+ "```\n\n"
68
+ )
69
+
70
+ if index % 10 == 0:
71
+ chunks.append(f"<section data-benchmark=\"{index}\">raw html block</section>\n\n")
72
+
73
+ return "".join(chunks)
74
+
75
+
76
+ def count_nodes(node: dict) -> int:
77
+ return 1 + sum(count_nodes(child) for child in node.get("content", []))
78
+
79
+
80
+ def mib(value: int) -> float:
81
+ return value / (1024 * 1024)
82
+
83
+
84
+ def main() -> None:
85
+ parser = argparse.ArgumentParser()
86
+ parser.add_argument("--sections", type=int, default=1_200)
87
+ parser.add_argument("--lorem-repetitions", type=int, default=4)
88
+ parser.add_argument("--threads", type=int, default=4)
89
+ args = parser.parse_args()
90
+
91
+ markdown = make_fixture(args.sections, args.lorem_repetitions)
92
+ input_bytes = len(markdown.encode("utf-8"))
93
+ size_mb = mib(input_bytes)
94
+
95
+ start = time.perf_counter()
96
+ doc = tm.from_markdown(markdown)
97
+ parse_seconds = time.perf_counter() - start
98
+
99
+ start = time.perf_counter()
100
+ ast = doc.to_dict()
101
+ to_dict_seconds = time.perf_counter() - start
102
+ node_count = count_nodes(ast)
103
+
104
+ tracemalloc.start()
105
+ tm.from_markdown(markdown)
106
+ _, peak_bytes = tracemalloc.get_traced_memory()
107
+ tracemalloc.stop()
108
+
109
+ start = time.perf_counter()
110
+ serialized = doc.to_markdown()
111
+ serialize_seconds = time.perf_counter() - start
112
+ output_bytes = len(serialized.encode("utf-8"))
113
+
114
+ with concurrent.futures.ThreadPoolExecutor(max_workers=args.threads) as pool:
115
+ start = time.perf_counter()
116
+ list(pool.map(tm.from_markdown, [markdown] * args.threads))
117
+ threaded_seconds = time.perf_counter() - start
118
+
119
+ print(f"sections: {args.sections}")
120
+ print(f"input: {size_mb:.2f} MiB")
121
+ print(f"output: {mib(output_bytes):.2f} MiB")
122
+ print(f"ast nodes: {node_count:,}")
123
+ print(f"markdown -> Document: {size_mb / parse_seconds:.2f} MiB/s ({parse_seconds:.3f}s)")
124
+ print(f"python peak during parse: {peak_bytes / (1024 * 1024):.2f} MiB")
125
+ print(f"Document -> Markdown: {size_mb / serialize_seconds:.2f} MiB/s ({serialize_seconds:.3f}s)")
126
+ print(f"Document -> dict: {node_count / to_dict_seconds:.0f} nodes/s ({to_dict_seconds:.3f}s)")
127
+ print(f"{args.threads} threaded parses: {(size_mb * args.threads) / threaded_seconds:.2f} MiB/s ({threaded_seconds:.3f}s)")
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()