wikinator 0.1.0__tar.gz → 0.2.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.
- {wikinator-0.1.0 → wikinator-0.2.0}/Makefile +4 -2
- {wikinator-0.1.0 → wikinator-0.2.0}/PKG-INFO +38 -4
- {wikinator-0.1.0 → wikinator-0.2.0}/README.md +36 -3
- {wikinator-0.1.0 → wikinator-0.2.0}/pyproject.toml +2 -1
- {wikinator-0.1.0 → wikinator-0.2.0}/src/wikinator/__about__.py +1 -1
- {wikinator-0.1.0 → wikinator-0.2.0}/src/wikinator/app.py +102 -12
- wikinator-0.2.0/src/wikinator/docxit.py +197 -0
- {wikinator-0.1.0 → wikinator-0.2.0}/uv.lock +28 -0
- {wikinator-0.1.0 → wikinator-0.2.0}/.github/workflows/release.yml +0 -0
- {wikinator-0.1.0 → wikinator-0.2.0}/.gitignore +0 -0
- {wikinator-0.1.0 → wikinator-0.2.0}/LICENSE +0 -0
- {wikinator-0.1.0 → wikinator-0.2.0}/src/wikinator/__init__.py +0 -0
- {wikinator-0.1.0 → wikinator-0.2.0}/src/wikinator/__main__.py +0 -0
- {wikinator-0.1.0 → wikinator-0.2.0}/src/wikinator/quickstart.py +0 -0
- {wikinator-0.1.0 → wikinator-0.2.0}/src/wikinator/wiki.py +0 -0
- {wikinator-0.1.0 → wikinator-0.2.0}/tests/smoke_test.py +0 -0
|
@@ -29,8 +29,10 @@ dist:
|
|
|
29
29
|
uv publish
|
|
30
30
|
|
|
31
31
|
clean:
|
|
32
|
-
rm -rf __pycache__
|
|
33
32
|
rm -fr .ruff_cache/
|
|
34
33
|
rm -fr dist/
|
|
34
|
+
rm -fr .venv/
|
|
35
35
|
rm -f dpytest_*.dat
|
|
36
|
-
|
|
36
|
+
rm -fr .pytest_cache/
|
|
37
|
+
find . -type f -name ‘*.pyc’ -delete
|
|
38
|
+
find . -name __pycache__ | xargs rm -rf
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wikinator
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Convert Google docs to markdown
|
|
5
5
|
Project-URL: Documentation, https://github.com/philion/wikinator#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/philion/wikinator/issues
|
|
7
7
|
Project-URL: Source, https://github.com/philion/wikinator
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Python: >=3.13
|
|
10
|
+
Requires-Dist: docx2markdown>=0.1.1
|
|
10
11
|
Requires-Dist: dotenv>=0.9.9
|
|
11
12
|
Requires-Dist: google-api-python-client>=2.175.0
|
|
12
13
|
Requires-Dist: google-auth-httplib2>=0.2.0
|
|
@@ -52,12 +53,12 @@ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql
|
|
|
52
53
|
## Build & Test
|
|
53
54
|
1. Clone
|
|
54
55
|
```
|
|
55
|
-
git clone
|
|
56
|
+
git clone https://github.com/philion/wikinator.git
|
|
56
57
|
cd wikinator
|
|
57
58
|
```
|
|
58
59
|
2. Run, with uv
|
|
59
60
|
```
|
|
60
|
-
uv run
|
|
61
|
+
uv run wikinator [options]
|
|
61
62
|
```
|
|
62
63
|
3. Test, with pytest
|
|
63
64
|
```
|
|
@@ -66,13 +67,42 @@ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql
|
|
|
66
67
|
|
|
67
68
|
## Development Log
|
|
68
69
|
|
|
70
|
+
### 2025-07-05
|
|
71
|
+
Starting work on image preservation.
|
|
72
|
+
|
|
73
|
+
Looking first at https://github.com/haesleinhuepf/docx2markdown for images.
|
|
74
|
+
|
|
75
|
+
Created a `Docx2MarkdownConverter` which almost works: images are put in the wrong path in the MD (`s/images/` instead of just `images`).
|
|
76
|
+
There's probably an easy fix, but lets try a pandoc version.
|
|
77
|
+
|
|
78
|
+
Creating `PandocConverter` to try and compare output.
|
|
79
|
+
```
|
|
80
|
+
pandoc {indoc} -f docx -t markdown --wrap=none --markdown-headings=atx --extract-media=images -o {outdoc}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Neither produces desired results.
|
|
84
|
+
|
|
85
|
+
Trying a literal hack of docx2markdown, to see how quickly I can fix the little problems I saw.
|
|
86
|
+
|
|
87
|
+
Got it working quickly, removed a little bug, got the images.
|
|
88
|
+
|
|
89
|
+
Now looking over the DOCX XML format to see how much I can scrape out.
|
|
90
|
+
|
|
91
|
+
https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.fontsizecomplexscript?view=openxml-3.0.1
|
|
92
|
+
|
|
93
|
+
Added detection for strikethru and Courier New (as "code font").
|
|
94
|
+
|
|
95
|
+
This is good enough for v0.2!
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
|
|
69
99
|
### 2025-07-04
|
|
70
100
|
Let's make a project! Today's goals:
|
|
71
101
|
- [x] clean up code and README
|
|
72
102
|
- [x] add CLI options, using type (not all implemented)
|
|
73
103
|
- [x] initial commit to github
|
|
74
104
|
- [ ] add image handling
|
|
75
|
-
- [
|
|
105
|
+
- [x] upload to pypi and confirm uvx commands
|
|
76
106
|
|
|
77
107
|
Cruft removed. README updated. (author waves, breaking 4th wall)
|
|
78
108
|
|
|
@@ -80,6 +110,10 @@ Moving on the main() cleanup and adding support for https://github.com/fastapi/t
|
|
|
80
110
|
|
|
81
111
|
Added simple CLI options for src and dest. Got end-to-end tree processing.
|
|
82
112
|
|
|
113
|
+
Added Makefile to help with release management. Got PyPI setup: https://pypi.org/project/wikinator/
|
|
114
|
+
|
|
115
|
+
`uvx wikinator` is working.
|
|
116
|
+
|
|
83
117
|
Let's go for git and call it a day!
|
|
84
118
|
|
|
85
119
|
### 2025-07-03
|
|
@@ -34,12 +34,12 @@ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql
|
|
|
34
34
|
## Build & Test
|
|
35
35
|
1. Clone
|
|
36
36
|
```
|
|
37
|
-
git clone
|
|
37
|
+
git clone https://github.com/philion/wikinator.git
|
|
38
38
|
cd wikinator
|
|
39
39
|
```
|
|
40
40
|
2. Run, with uv
|
|
41
41
|
```
|
|
42
|
-
uv run
|
|
42
|
+
uv run wikinator [options]
|
|
43
43
|
```
|
|
44
44
|
3. Test, with pytest
|
|
45
45
|
```
|
|
@@ -48,13 +48,42 @@ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql
|
|
|
48
48
|
|
|
49
49
|
## Development Log
|
|
50
50
|
|
|
51
|
+
### 2025-07-05
|
|
52
|
+
Starting work on image preservation.
|
|
53
|
+
|
|
54
|
+
Looking first at https://github.com/haesleinhuepf/docx2markdown for images.
|
|
55
|
+
|
|
56
|
+
Created a `Docx2MarkdownConverter` which almost works: images are put in the wrong path in the MD (`s/images/` instead of just `images`).
|
|
57
|
+
There's probably an easy fix, but lets try a pandoc version.
|
|
58
|
+
|
|
59
|
+
Creating `PandocConverter` to try and compare output.
|
|
60
|
+
```
|
|
61
|
+
pandoc {indoc} -f docx -t markdown --wrap=none --markdown-headings=atx --extract-media=images -o {outdoc}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Neither produces desired results.
|
|
65
|
+
|
|
66
|
+
Trying a literal hack of docx2markdown, to see how quickly I can fix the little problems I saw.
|
|
67
|
+
|
|
68
|
+
Got it working quickly, removed a little bug, got the images.
|
|
69
|
+
|
|
70
|
+
Now looking over the DOCX XML format to see how much I can scrape out.
|
|
71
|
+
|
|
72
|
+
https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.fontsizecomplexscript?view=openxml-3.0.1
|
|
73
|
+
|
|
74
|
+
Added detection for strikethru and Courier New (as "code font").
|
|
75
|
+
|
|
76
|
+
This is good enough for v0.2!
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
|
|
51
80
|
### 2025-07-04
|
|
52
81
|
Let's make a project! Today's goals:
|
|
53
82
|
- [x] clean up code and README
|
|
54
83
|
- [x] add CLI options, using type (not all implemented)
|
|
55
84
|
- [x] initial commit to github
|
|
56
85
|
- [ ] add image handling
|
|
57
|
-
- [
|
|
86
|
+
- [x] upload to pypi and confirm uvx commands
|
|
58
87
|
|
|
59
88
|
Cruft removed. README updated. (author waves, breaking 4th wall)
|
|
60
89
|
|
|
@@ -62,6 +91,10 @@ Moving on the main() cleanup and adding support for https://github.com/fastapi/t
|
|
|
62
91
|
|
|
63
92
|
Added simple CLI options for src and dest. Got end-to-end tree processing.
|
|
64
93
|
|
|
94
|
+
Added Makefile to help with release management. Got PyPI setup: https://pypi.org/project/wikinator/
|
|
95
|
+
|
|
96
|
+
`uvx wikinator` is working.
|
|
97
|
+
|
|
65
98
|
Let's go for git and call it a day!
|
|
66
99
|
|
|
67
100
|
### 2025-07-03
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "wikinator"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Convert Google docs to markdown"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
7
7
|
dependencies = [
|
|
8
|
+
"docx2markdown>=0.1.1",
|
|
8
9
|
"dotenv>=0.9.9",
|
|
9
10
|
"google-api-python-client>=2.175.0",
|
|
10
11
|
"google-auth-httplib2>=0.2.0",
|
|
@@ -2,11 +2,15 @@ import os
|
|
|
2
2
|
from pathlib import Path, PurePath
|
|
3
3
|
import logging
|
|
4
4
|
import json
|
|
5
|
+
import subprocess
|
|
5
6
|
|
|
6
7
|
from markitdown import MarkItDown
|
|
8
|
+
import docx2markdown
|
|
7
9
|
|
|
8
10
|
import typer
|
|
9
11
|
|
|
12
|
+
from wikinator import docxit
|
|
13
|
+
|
|
10
14
|
class Page:
|
|
11
15
|
"""
|
|
12
16
|
Specific class for page, to help with validation, as graphql is strict about params
|
|
@@ -101,6 +105,7 @@ class Converter:
|
|
|
101
105
|
logging.debug(f"No processor for {ext}, skipping {full_path}")
|
|
102
106
|
|
|
103
107
|
|
|
108
|
+
# remove
|
|
104
109
|
class MarkitdownConverter(Converter):
|
|
105
110
|
def _convert(self, filepath:Path) -> Page:
|
|
106
111
|
"""
|
|
@@ -126,21 +131,106 @@ class MarkitdownConverter(Converter):
|
|
|
126
131
|
isPrivate = True,
|
|
127
132
|
)
|
|
128
133
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
#
|
|
134
|
+
# remove
|
|
135
|
+
class Docx2MarkdownConverter(Converter):
|
|
136
|
+
def convert(self, infile:Path, outroot:Path) -> Page:
|
|
137
|
+
"""
|
|
138
|
+
Converts a docx file into markdown using docx2markdown
|
|
139
|
+
"""
|
|
140
|
+
rel_path = PurePath(os.path.relpath(infile, self.root.parent)) # remove .parent to remove top dirname
|
|
141
|
+
rel_file = PurePath(rel_path.parent, infile.stem + ".md")
|
|
142
|
+
out_file = Path(outroot, rel_file)
|
|
143
|
+
|
|
144
|
+
#print(f"in: {infile}, out: {out_file}")
|
|
145
|
+
|
|
146
|
+
# FIXME deconstruct docx_to_markdown to seperate convert from write
|
|
147
|
+
# assure required dirs exist
|
|
148
|
+
out_file.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
docx2markdown.docx_to_markdown(infile, out_file)
|
|
150
|
+
|
|
151
|
+
return Page(
|
|
152
|
+
title = infile.stem,
|
|
153
|
+
path = str(rel_file),
|
|
154
|
+
content = "", ## FIXME
|
|
155
|
+
editor = "markdown",
|
|
156
|
+
locale = "en",
|
|
157
|
+
tags = None,
|
|
158
|
+
description = f"generated from: {infile}",
|
|
159
|
+
isPublished = False,
|
|
160
|
+
isPrivate = True,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# remove?
|
|
164
|
+
class PandocConverter(Converter):
|
|
165
|
+
def convert(self, infile:Path, outroot:Path) -> Page:
|
|
166
|
+
"""
|
|
167
|
+
Converts a docx file into markdown using docx2markdown
|
|
168
|
+
"""
|
|
169
|
+
rel_path = PurePath(os.path.relpath(infile, self.root.parent)) # remove .parent to remove top dirname
|
|
170
|
+
rel_file = PurePath(rel_path.parent, infile.stem + ".md")
|
|
171
|
+
out_file = Path(outroot, rel_file)
|
|
172
|
+
|
|
173
|
+
media_path = Path(outroot, "images")
|
|
174
|
+
|
|
175
|
+
# assure required dirs exist
|
|
176
|
+
out_file.parent.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
|
|
178
|
+
# pandoc {indoc} -f docx -t markdown --wrap=none --markdown-headings=atx --extract-media=images -o {outdoc}
|
|
179
|
+
command = ["pandoc", infile,
|
|
180
|
+
"-f", "docx",
|
|
181
|
+
"-t", "markdown",
|
|
182
|
+
"--wrap=none",
|
|
183
|
+
"--markdown-headings=atx",
|
|
184
|
+
"--extract-media=" + str(media_path),
|
|
185
|
+
"-o", out_file]
|
|
186
|
+
subprocess.run(command, check=True)
|
|
187
|
+
|
|
188
|
+
return Page(
|
|
189
|
+
title = infile.stem,
|
|
190
|
+
path = str(rel_file),
|
|
191
|
+
content = "", ## FIXME
|
|
192
|
+
editor = "markdown",
|
|
193
|
+
locale = "en",
|
|
194
|
+
tags = None,
|
|
195
|
+
description = f"generated from: {infile}",
|
|
196
|
+
isPublished = False,
|
|
197
|
+
isPrivate = True,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class DocxitConverter(Converter):
|
|
202
|
+
def convert(self, infile:Path, outroot:Path) -> Page:
|
|
203
|
+
"""
|
|
204
|
+
Converts a docx file into markdown using docxit
|
|
205
|
+
"""
|
|
206
|
+
rel_path = PurePath(os.path.relpath(infile, self.root.parent)) # remove .parent to remove top dirname
|
|
207
|
+
rel_file = PurePath(rel_path.parent, infile.stem + ".md")
|
|
208
|
+
out_file = Path(outroot, rel_file)
|
|
209
|
+
|
|
210
|
+
# FIXME deconstruct docx_to_markdown to seperate convert from write
|
|
211
|
+
# assure required dirs exist
|
|
212
|
+
out_file.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
docxit.docx_to_markdown(infile, out_file)
|
|
214
|
+
|
|
215
|
+
return Page(
|
|
216
|
+
title = infile.stem,
|
|
217
|
+
path = str(rel_file),
|
|
218
|
+
content = "", ## FIXME
|
|
219
|
+
editor = "markdown",
|
|
220
|
+
locale = "en",
|
|
221
|
+
tags = None,
|
|
222
|
+
description = f"generated from: {infile}",
|
|
223
|
+
isPublished = False,
|
|
224
|
+
isPrivate = True,
|
|
225
|
+
)
|
|
140
226
|
|
|
141
227
|
|
|
142
228
|
def do_convert(src: str, dest: str) -> None:
|
|
143
|
-
MarkitdownConverter().convert_directory(src, dest)
|
|
229
|
+
#MarkitdownConverter().convert_directory(src, dest)
|
|
230
|
+
#Docx2MarkdownConverter().convert_directory(src, dest)
|
|
231
|
+
#PandocConverter().convert_directory(src, dest)
|
|
232
|
+
DocxitConverter().convert_directory(src, dest)
|
|
233
|
+
|
|
144
234
|
|
|
145
235
|
|
|
146
236
|
def main() -> None:
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# This file is original taken from https://github.com/haesleinhuepf/docx2markdown
|
|
2
|
+
# - src/docx2markdown/_docx_to_markdown.py
|
|
3
|
+
# It is being altered here to provide results needed for this project.
|
|
4
|
+
# original license: https://github.com/haesleinhuepf/docx2markdown/blob/main/LICENSE
|
|
5
|
+
# included here for completeness
|
|
6
|
+
#---
|
|
7
|
+
# BSD 3-Clause License
|
|
8
|
+
# Copyright (c) 2024, Robert Haase, ScaDS.AI, Uni Leipzig
|
|
9
|
+
# Redistribution and use in source and binary forms, with or without
|
|
10
|
+
# modification, are permitted provided that the following conditions are met:
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
12
|
+
# list of conditions and the following disclaimer.
|
|
13
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
# and/or other materials provided with the distribution.
|
|
16
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
# contributors may be used to endorse or promote products derived from
|
|
18
|
+
# this software without specific prior written permission.
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
29
|
+
#---
|
|
30
|
+
# All changes in the this version are Copyright (c) 2025, Paul Philion, Acme Rocket Company
|
|
31
|
+
# under the provided MIT license.
|
|
32
|
+
|
|
33
|
+
import docx
|
|
34
|
+
import os
|
|
35
|
+
from lxml import etree
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
def docx_to_markdown(docx_file, output_md):
|
|
39
|
+
"""Convert a .docx file to a Markdown file and a subfolder of images."""
|
|
40
|
+
|
|
41
|
+
folder = str(Path(output_md).parent)
|
|
42
|
+
image_folder = str(Path(output_md).parent / "images")
|
|
43
|
+
|
|
44
|
+
doc = docx.Document(docx_file)
|
|
45
|
+
|
|
46
|
+
paragraphs = list(doc.paragraphs)
|
|
47
|
+
tables = list(doc.tables)
|
|
48
|
+
markdown = []
|
|
49
|
+
|
|
50
|
+
# save all images
|
|
51
|
+
images = {}
|
|
52
|
+
for rel in doc.part.rels.values():
|
|
53
|
+
if "image" in rel.reltype:
|
|
54
|
+
image_filename = save_image(rel.target_part, image_folder)
|
|
55
|
+
images[rel.rId] = image_filename[len(folder)+1:]
|
|
56
|
+
#print("image file", image_filename, "-->", images[rel.rId])
|
|
57
|
+
|
|
58
|
+
#print("images", images)
|
|
59
|
+
|
|
60
|
+
for block in doc.element.body:
|
|
61
|
+
if block.tag.endswith('p'): # Handle paragraphs
|
|
62
|
+
paragraph = paragraphs.pop(0) # Match current paragraph
|
|
63
|
+
md_paragraph = ""
|
|
64
|
+
|
|
65
|
+
### switching on paragraph.style.name
|
|
66
|
+
|
|
67
|
+
style_name = paragraph.style.name
|
|
68
|
+
|
|
69
|
+
if "List" in style_name:
|
|
70
|
+
prefix = get_bullet_point_prefix(paragraph)
|
|
71
|
+
md_paragraph = prefix # Markdown syntax for bullet points
|
|
72
|
+
elif "Heading 1" in style_name:
|
|
73
|
+
md_paragraph = "# "
|
|
74
|
+
elif "Heading 2" in style_name:
|
|
75
|
+
md_paragraph = "## "
|
|
76
|
+
elif "Heading 3" in style_name:
|
|
77
|
+
md_paragraph = "### "
|
|
78
|
+
elif "Heading 4" in style_name:
|
|
79
|
+
md_paragraph = "#### "
|
|
80
|
+
elif "Heading 5" in style_name:
|
|
81
|
+
md_paragraph = "##### "
|
|
82
|
+
|
|
83
|
+
elif "Normal" or "normal" in style_name:
|
|
84
|
+
md_paragraph = ""
|
|
85
|
+
else:
|
|
86
|
+
print("Unsupported style:", style_name)
|
|
87
|
+
|
|
88
|
+
md_paragraph += parse_run(paragraph, images)
|
|
89
|
+
|
|
90
|
+
markdown.append(md_paragraph)
|
|
91
|
+
|
|
92
|
+
elif block.tag.endswith('tbl'): # Handle tables (if present)
|
|
93
|
+
table = tables.pop(0) # Match current table
|
|
94
|
+
table_text = ""
|
|
95
|
+
for i, row in enumerate(table.rows):
|
|
96
|
+
table_text += "| " + " | ".join(cell.text.strip() for cell in row.cells) + " |\n"
|
|
97
|
+
if i == 0:
|
|
98
|
+
table_text += "| " + " | ".join("---" for _ in row.cells) + " |\n"
|
|
99
|
+
|
|
100
|
+
markdown.append(table_text)
|
|
101
|
+
|
|
102
|
+
elif block.tag.endswith('sectPr') or block.tag.endswith('sdt'):
|
|
103
|
+
# ignore
|
|
104
|
+
pass
|
|
105
|
+
else:
|
|
106
|
+
print("Unsupported block:", docx_file, block.tag)
|
|
107
|
+
|
|
108
|
+
# Write to Markdown file
|
|
109
|
+
with open(output_md, "w", encoding="utf-8") as md_file:
|
|
110
|
+
md_file.write("\n\n".join(markdown))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def extract_r_embed(xml_string):
|
|
114
|
+
"""
|
|
115
|
+
Extract the value of r:embed from the given XML string.
|
|
116
|
+
|
|
117
|
+
:param xml_string: The XML content as a string.
|
|
118
|
+
:return: The value of r:embed or None if not found.
|
|
119
|
+
"""
|
|
120
|
+
# Parse the XML
|
|
121
|
+
root = etree.fromstring(xml_string)
|
|
122
|
+
|
|
123
|
+
# Define the namespaces
|
|
124
|
+
namespaces = {
|
|
125
|
+
'a': "http://schemas.openxmlformats.org/drawingml/2006/main",
|
|
126
|
+
'r': "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
|
127
|
+
'pic': "http://schemas.openxmlformats.org/drawingml/2006/picture",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Use XPath to find the <a:blip> element with r:embed
|
|
131
|
+
blip = root.find(".//a:blip", namespaces=namespaces)
|
|
132
|
+
|
|
133
|
+
# Extract the r:embed attribute value
|
|
134
|
+
if blip is not None:
|
|
135
|
+
return blip.attrib.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed")
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
def save_image(image_part, output_folder):
|
|
139
|
+
"""Save an image to the output folder and return the filename."""
|
|
140
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
141
|
+
image_filename = os.path.join(output_folder, os.path.basename(image_part.partname))
|
|
142
|
+
with open(image_filename, "wb") as img_file:
|
|
143
|
+
img_file.write(image_part.blob)
|
|
144
|
+
return str(image_filename).replace("\\", "/")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_list_level(paragraph):
|
|
148
|
+
"""Determine the level of a bullet point or numbered list item."""
|
|
149
|
+
# Access the raw XML of the paragraph
|
|
150
|
+
p = paragraph._element
|
|
151
|
+
numPr = p.find(".//w:numPr", namespaces=p.nsmap)
|
|
152
|
+
if numPr is not None:
|
|
153
|
+
ilvl = numPr.find(".//w:ilvl", namespaces=p.nsmap)
|
|
154
|
+
if ilvl is not None:
|
|
155
|
+
return int(ilvl.get("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val"))
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
def get_bullet_point_prefix(paragraph):
|
|
159
|
+
"""Determine the Markdown prefix for a bullet point based on its indentation level."""
|
|
160
|
+
level = get_list_level(paragraph)
|
|
161
|
+
return " " * level + "- " # Use Markdown syntax for nested lists
|
|
162
|
+
|
|
163
|
+
def parse_run(run, images):
|
|
164
|
+
"""Go through document objects recursively and return markdown."""
|
|
165
|
+
sub_parts = list(run.iter_inner_content())
|
|
166
|
+
text = ""
|
|
167
|
+
for s in sub_parts:
|
|
168
|
+
if isinstance(s, str):
|
|
169
|
+
text += s
|
|
170
|
+
elif isinstance(s, docx.text.run.Run):
|
|
171
|
+
text += parse_run(s, images)
|
|
172
|
+
elif isinstance(s, docx.text.hyperlink.Hyperlink):
|
|
173
|
+
text += f"[{s.text}]({s.address})"
|
|
174
|
+
elif isinstance(s, docx.drawing.Drawing):
|
|
175
|
+
rId = extract_r_embed(s._element.xml)
|
|
176
|
+
image_url = images[rId]
|
|
177
|
+
text += f""
|
|
178
|
+
else:
|
|
179
|
+
print("unknown run type", s)
|
|
180
|
+
|
|
181
|
+
if isinstance(run, docx.text.run.Run):
|
|
182
|
+
if run.bold:
|
|
183
|
+
text = f"**{text}**"
|
|
184
|
+
if run.italic:
|
|
185
|
+
text = f"*{text}*"
|
|
186
|
+
if run.underline:
|
|
187
|
+
text = f"__{text}__"
|
|
188
|
+
if run.font.strike:
|
|
189
|
+
text = f"~~{text}~~"
|
|
190
|
+
# check .font for monospacing
|
|
191
|
+
# check style
|
|
192
|
+
if run.font.name == "Courier New":
|
|
193
|
+
print(text)
|
|
194
|
+
text = f"`{text}`"
|
|
195
|
+
#print(run.font.name)
|
|
196
|
+
|
|
197
|
+
return text
|
|
@@ -347,6 +347,19 @@ wheels = [
|
|
|
347
347
|
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
|
348
348
|
]
|
|
349
349
|
|
|
350
|
+
[[package]]
|
|
351
|
+
name = "docx2markdown"
|
|
352
|
+
version = "0.1.1"
|
|
353
|
+
source = { registry = "https://pypi.org/simple" }
|
|
354
|
+
dependencies = [
|
|
355
|
+
{ name = "lxml" },
|
|
356
|
+
{ name = "python-docx" },
|
|
357
|
+
]
|
|
358
|
+
sdist = { url = "https://files.pythonhosted.org/packages/06/be/692425e45455b1bbff7ad0b9fa2fab31ac3f9da13f210ded06c0c99158f5/docx2markdown-0.1.1.tar.gz", hash = "sha256:177e7ca0c998139b9afb9ffbe9a3be05a27d60664ac602b38de643e7b343593d", size = 8256, upload-time = "2024-11-23T21:11:45.015Z" }
|
|
359
|
+
wheels = [
|
|
360
|
+
{ url = "https://files.pythonhosted.org/packages/d2/f5/970f7c2f64331891ff8b8c1863a793779e4fc784dbd210e8ae3712bcfd0e/docx2markdown-0.1.1-py3-none-any.whl", hash = "sha256:a97d413e6a225d4a0932cc8660b92f10f5de28f0436bc18ee12ef3961cd4b2b1", size = 8393, upload-time = "2024-11-23T21:11:43.452Z" },
|
|
361
|
+
]
|
|
362
|
+
|
|
350
363
|
[[package]]
|
|
351
364
|
name = "dotenv"
|
|
352
365
|
version = "0.9.9"
|
|
@@ -1141,6 +1154,19 @@ wheels = [
|
|
|
1141
1154
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
|
1142
1155
|
]
|
|
1143
1156
|
|
|
1157
|
+
[[package]]
|
|
1158
|
+
name = "python-docx"
|
|
1159
|
+
version = "1.2.0"
|
|
1160
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1161
|
+
dependencies = [
|
|
1162
|
+
{ name = "lxml" },
|
|
1163
|
+
{ name = "typing-extensions" },
|
|
1164
|
+
]
|
|
1165
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" }
|
|
1166
|
+
wheels = [
|
|
1167
|
+
{ url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" },
|
|
1168
|
+
]
|
|
1169
|
+
|
|
1144
1170
|
[[package]]
|
|
1145
1171
|
name = "python-dotenv"
|
|
1146
1172
|
version = "1.1.1"
|
|
@@ -1364,6 +1390,7 @@ name = "wikinator"
|
|
|
1364
1390
|
version = "0.1.0"
|
|
1365
1391
|
source = { editable = "." }
|
|
1366
1392
|
dependencies = [
|
|
1393
|
+
{ name = "docx2markdown" },
|
|
1367
1394
|
{ name = "dotenv" },
|
|
1368
1395
|
{ name = "google-api-python-client" },
|
|
1369
1396
|
{ name = "google-auth-httplib2" },
|
|
@@ -1375,6 +1402,7 @@ dependencies = [
|
|
|
1375
1402
|
|
|
1376
1403
|
[package.metadata]
|
|
1377
1404
|
requires-dist = [
|
|
1405
|
+
{ name = "docx2markdown", specifier = ">=0.1.1" },
|
|
1378
1406
|
{ name = "dotenv", specifier = ">=0.9.9" },
|
|
1379
1407
|
{ name = "google-api-python-client", specifier = ">=2.175.0" },
|
|
1380
1408
|
{ name = "google-auth-httplib2", specifier = ">=0.2.0" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|