markitdown-cosense 0.1.0__py3-none-any.whl

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,10 @@
1
+ """markitdown-cosense: A markitdown plugin for converting Scrapbox to Markdown."""
2
+
3
+ from ._plugin import __plugin_interface_version__, register_converters
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = [
7
+ "register_converters",
8
+ "__plugin_interface_version__",
9
+ "__version__",
10
+ ]
@@ -0,0 +1,366 @@
1
+ import re
2
+ from typing import (
3
+ Any,
4
+ BinaryIO,
5
+ Dict,
6
+ List,
7
+ Tuple,
8
+ )
9
+
10
+ from markitdown import (
11
+ DocumentConverter,
12
+ DocumentConverterResult,
13
+ MarkItDown,
14
+ StreamInfo,
15
+ )
16
+
17
+ __plugin_interface_version__ = 1
18
+
19
+
20
+ ACCEPTED_FILE_EXTENSIONS = [".txt"]
21
+ IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "svg", "webp"]
22
+ CODE_BLOCK_PREFIX = "code:"
23
+ CODE_BLOCK_PLACEHOLDER = "<<<CODEBLOCK{}>>>"
24
+ CODE_BLOCK_PATTERN = re.compile(r"```[\s\S]*?```", re.MULTILINE)
25
+
26
+ _image_extensions_pattern = None
27
+
28
+
29
+ def register_converters(markitdown: MarkItDown, **kwargs) -> None:
30
+ markitdown.register_converter(MarkdownConverter())
31
+
32
+
33
+ class MarkdownConverter(DocumentConverter):
34
+ def accepts(
35
+ self, _file_stream: BinaryIO, stream_info: StreamInfo, **kwargs
36
+ ) -> bool:
37
+ extension = (stream_info.extension or "").lower()
38
+ return extension in ACCEPTED_FILE_EXTENSIONS
39
+
40
+ def convert(
41
+ self, file_stream: BinaryIO, stream_info: StreamInfo, **kwargs
42
+ ) -> DocumentConverterResult:
43
+ file_stream.seek(0)
44
+ content = file_stream.read().decode("utf-8")
45
+
46
+ return DocumentConverterResult(_convert_content(content))
47
+
48
+
49
+ def _convert_content(content: str) -> str:
50
+ content = convert_code_blocks(content)
51
+ content, protected_blocks = protect_code_blocks(content)
52
+
53
+ content = convert_tables(content)
54
+ content = convert_lists(content)
55
+ content = apply_conversions(content)
56
+
57
+ return restore_code_blocks(content, protected_blocks)
58
+
59
+
60
+ def convert_code_blocks(content: str) -> str:
61
+ lines = content.splitlines()
62
+ result = []
63
+ i = 0
64
+
65
+ while i < len(lines):
66
+ line = lines[i]
67
+ stripped = line.lstrip(" \t ")
68
+
69
+ if stripped.startswith(CODE_BLOCK_PREFIX):
70
+ indent = line[: len(line) - len(stripped)]
71
+ filename = stripped[len(CODE_BLOCK_PREFIX) :].strip()
72
+
73
+ code_lines, next_i = _collect_code_block_lines(lines, i + 1)
74
+
75
+ if filename == "tex":
76
+ _process_latex_block(code_lines, result, indent)
77
+ else:
78
+ if filename:
79
+ parts = filename.rsplit(".", 1)
80
+ lang = parts[1] if len(parts) == 2 else filename
81
+ else:
82
+ lang = ""
83
+ _add_code_block(result, lang, code_lines, bool(indent))
84
+
85
+ i = next_i
86
+ else:
87
+ result.append(line)
88
+ i += 1
89
+
90
+ return "\n".join(result)
91
+
92
+
93
+ def _collect_code_block_lines(
94
+ lines: List[str], start_index: int
95
+ ) -> Tuple[List[str], int]:
96
+ code_lines = []
97
+ base_indent = (
98
+ calculate_base_indentation(lines[start_index])
99
+ if start_index < len(lines) and lines[start_index].strip()
100
+ else 0
101
+ )
102
+ consecutive_empty_lines = 0
103
+
104
+ for i in range(start_index, len(lines)):
105
+ line = lines[i]
106
+
107
+ # Count consecutive empty lines
108
+ if not line.strip():
109
+ consecutive_empty_lines += 1
110
+ # End block after 2 consecutive empty lines
111
+ if consecutive_empty_lines >= 2:
112
+ return code_lines[:-1], i # Remove the last empty line
113
+ else:
114
+ consecutive_empty_lines = 0
115
+
116
+ # Check for block end conditions
117
+ if line.strip() and (
118
+ calculate_base_indentation(line) < base_indent
119
+ or line.lstrip(" \t ").startswith(CODE_BLOCK_PREFIX)
120
+ ):
121
+ return code_lines, i
122
+
123
+ code_lines.append(line)
124
+
125
+ return code_lines, len(lines)
126
+
127
+
128
+ def protect_code_blocks(content: str) -> Tuple[str, List[str]]:
129
+ code_blocks = CODE_BLOCK_PATTERN.findall(content)
130
+
131
+ def replacer(match):
132
+ index = replacer.counter
133
+ replacer.counter += 1
134
+ return CODE_BLOCK_PLACEHOLDER.format(index)
135
+
136
+ replacer.counter = 0
137
+ protected_content = CODE_BLOCK_PATTERN.sub(replacer, content)
138
+
139
+ return protected_content, code_blocks
140
+
141
+
142
+ def restore_code_blocks(content: str, code_blocks: List[str]) -> str:
143
+ for i, code_block in enumerate(code_blocks):
144
+ content = content.replace(CODE_BLOCK_PLACEHOLDER.format(i), code_block)
145
+ return content
146
+
147
+
148
+ def get_image_extensions_pattern() -> re.Pattern[str]:
149
+ global _image_extensions_pattern
150
+ if _image_extensions_pattern is None:
151
+ extensions = "|".join(IMAGE_EXTENSIONS)
152
+ _image_extensions_pattern = re.compile(
153
+ rf"\[(https?://[^\s\]]+\.(?:{extensions}))\]"
154
+ )
155
+ return _image_extensions_pattern
156
+
157
+
158
+ def calculate_base_indentation(line: str) -> int:
159
+ for i, char in enumerate(line):
160
+ if char not in (" ", "\t", " "):
161
+ return i
162
+ return len(line)
163
+
164
+
165
+ def is_indented_line(line: str) -> bool:
166
+ return bool(line and line[0] in (" ", "\t", " "))
167
+
168
+
169
+ CONVERSION_PATTERNS = [
170
+ # Tag pattern - must come first to avoid conflicts
171
+ (
172
+ r"\[(?!\*|img\s|/\s|-\s|\$|https?://|YouTube\s|Twitter\s|\w+\s+https?://)([^\[\]/\-\*\s][^\[\]/\-\*\]]*?)(?!\s+https?://)\]",
173
+ r"<!-- tag: \1 -->",
174
+ ),
175
+ # Formatting patterns
176
+ (r"\[\*/\s*(.*?)\]", r"***\1***"),
177
+ (r"\[\*-\s*(.*?)\]", r"**~~\1~~**"),
178
+ (r"\[/-\s*(.*?)\]", r"*~~\1~~*"),
179
+ (r"\[\*\*\*\s*(.*?)\s*\*\*\*\]", r"**\1**"),
180
+ (r"\[\*\*\s*(.*?)\s*\*\*\]", r"**\1**"),
181
+ # Heading patterns
182
+ (r"\[\*\*\*\*\*\s*(.*?)\]", r"##### \1"),
183
+ (r"\[\*\*\*\*\s*(.*?)\]", r"#### \1"),
184
+ (r"\[\*\*\*\s*(.*?)\]", r"### \1"),
185
+ (r"\[\*\*\s*(.*?)\]", r"## \1"),
186
+ (r"\[\*\s*(.*?)\]", r"# \1"),
187
+ # Basic formatting
188
+ (r"\[/\s*(.*?)\]", r"*\1*"),
189
+ (r"\[-\s*(.*?)\]", r"~~\1~~"),
190
+ (r"\[\$\s*(.*?)\s*\$\]", r"$\1$"),
191
+ # Images and media
192
+ (r"\[img\s+(https?://[^\s\]]+)\]", r"![img](\1)"),
193
+ ("_DYNAMIC_IMAGE_PATTERN_", r"![](\1)"), # Special marker for dynamic pattern
194
+ (
195
+ r"\[YouTube\s+(https?://(?:www\.)?youtube\.com/watch\?v=[\w-]+|https?://youtu\.be/[\w-]+)\]",
196
+ r"[YouTube Video](\1)",
197
+ ),
198
+ (
199
+ r"\[Twitter\s+(https?://(?:www\.)?twitter\.com/\w+/status/\d+|https?://x\.com/\w+/status/\d+)\]",
200
+ r"[Twitter Post](\1)",
201
+ ),
202
+ # Links
203
+ (r"\[([^/\-\*\]]+?)\s+(https?://[^\s\]]+)\]", r"[\1](\2)"),
204
+ (r"\[(https?://[^\s\]]+)\s+([^/\-\*\]]+?)\]", r"[\2](\1)"),
205
+ # URL auto-linking
206
+ (r'(?<!\()(https?://[^\s<>"\']+(?:\([^\s<>"\']*\)|[^\s<>"\']*)*)', r"<\1>"),
207
+ # Blockquotes
208
+ (r"^>\s*(.*)$", r"> \1"),
209
+ ]
210
+
211
+ _compiled_patterns: List[Tuple[re.Pattern[str], str]] | None = None
212
+
213
+
214
+ def _get_compiled_patterns() -> List[Tuple[re.Pattern[str], str]]:
215
+ global _compiled_patterns
216
+ if _compiled_patterns is None:
217
+ compiled = []
218
+ for pattern_str, replacement in CONVERSION_PATTERNS:
219
+ if pattern_str == "_DYNAMIC_IMAGE_PATTERN_":
220
+ pattern = get_image_extensions_pattern()
221
+ else:
222
+ pattern = re.compile(pattern_str, re.MULTILINE)
223
+ compiled.append((pattern, replacement))
224
+ _compiled_patterns = compiled
225
+ return _compiled_patterns
226
+
227
+
228
+ def apply_conversions(content: str) -> str:
229
+ for pattern, replacement in _get_compiled_patterns():
230
+ content = pattern.sub(replacement, content)
231
+ return content
232
+
233
+
234
+ def _process_latex_block(
235
+ code_lines: List[str], result: List[str], leading_indent: str
236
+ ) -> None:
237
+ math_operators = set("=+-*/^")
238
+ math_indicators = ["E(", "V(", "Cov(", "σ", "μ", "√", "Φ", "\\", "^2", "_"]
239
+ excluded_prefixes = ("![", "http", "<http", "->")
240
+
241
+ for code_line in code_lines:
242
+ stripped_line = code_line.strip()
243
+ if not stripped_line:
244
+ result.append("")
245
+ continue
246
+
247
+ has_math = any(c in stripped_line for c in math_operators) or any(
248
+ indicator in stripped_line for indicator in math_indicators
249
+ )
250
+
251
+ if (
252
+ not has_math
253
+ or stripped_line.startswith(excluded_prefixes)
254
+ or stripped_line == "code:tex"
255
+ ):
256
+ result.append(code_line.rstrip())
257
+ continue
258
+
259
+ has_japanese = any(
260
+ "\u3040" <= c <= "\u309f"
261
+ or "\u30a0" <= c <= "\u30ff"
262
+ or "\u4e00" <= c <= "\u9faf"
263
+ for c in stripped_line
264
+ )
265
+
266
+ if has_japanese:
267
+ result.append(code_line.rstrip())
268
+ else:
269
+ result.append(f"{leading_indent}${stripped_line}$")
270
+
271
+
272
+ def _add_code_block(
273
+ result: List[str],
274
+ lang: str,
275
+ code_lines: List[str],
276
+ has_leading_indent: bool = False,
277
+ ) -> None:
278
+ if has_leading_indent:
279
+ result.append("")
280
+
281
+ result.append(f"```{lang}")
282
+ result.extend(line if line.strip() else "" for line in code_lines)
283
+ result.append("```")
284
+
285
+ if has_leading_indent:
286
+ result.append("")
287
+
288
+
289
+ def convert_lists(content: str) -> str:
290
+ lines = content.splitlines()
291
+ result = []
292
+
293
+ for line in lines:
294
+ if is_indented_line(line):
295
+ indent_chars = 0
296
+ for char in line:
297
+ if char in (" ", "\t", " "):
298
+ indent_chars += 1
299
+ else:
300
+ break
301
+
302
+ indent_level = max(0, indent_chars - 1)
303
+ content = line[indent_chars:]
304
+ markdown_indent = " " * (2 * indent_level)
305
+ result.append(f"{markdown_indent}- {content}")
306
+ else:
307
+ result.append(line)
308
+
309
+ return "\n".join(result)
310
+
311
+
312
+ def convert_tables(content: str) -> str:
313
+ lines = content.splitlines()
314
+ result = []
315
+ i = 0
316
+
317
+ while i < len(lines):
318
+ line = lines[i]
319
+
320
+ if line.startswith("table:"):
321
+ processed_table = _process_table_block(lines, i)
322
+ result.extend(processed_table["content"])
323
+ i = processed_table["next_index"]
324
+ continue
325
+
326
+ result.append(line)
327
+ i += 1
328
+
329
+ return "\n".join(result)
330
+
331
+
332
+ def _process_table_block(lines: List[str], start_index: int) -> Dict[str, Any]:
333
+ line = lines[start_index]
334
+ result_content = []
335
+
336
+ table_name = line[6:].strip()
337
+ if table_name:
338
+ result_content.append(f"## {table_name}")
339
+ result_content.append("")
340
+
341
+ table_rows = []
342
+ i = start_index + 1
343
+ while i < len(lines) and lines[i].startswith((" ", "\t")):
344
+ row_content = lines[i].strip()
345
+ if row_content:
346
+ table_rows.append(row_content)
347
+ i += 1
348
+
349
+ if table_rows:
350
+ header_row = table_rows[0]
351
+ columns = [col.strip() for col in header_row.split()]
352
+
353
+ if columns:
354
+ result_content.append("| " + " | ".join(columns) + " |")
355
+ result_content.append("|" + "---|" * len(columns))
356
+
357
+ for row in table_rows[1:]:
358
+ data_columns = [col.strip() for col in row.split()]
359
+ while len(data_columns) < len(columns):
360
+ data_columns.append("")
361
+ data_columns = data_columns[: len(columns)]
362
+ result_content.append("| " + " | ".join(data_columns) + " |")
363
+
364
+ result_content.append("")
365
+
366
+ return {"content": result_content, "next_index": i}
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: markitdown-cosense
3
+ Version: 0.1.0
4
+ Summary: A markitdown plugin for converting Scrapbox notation to Markdown
5
+ Author-email: kazu728 <kazuki.matsuo.728@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kazu728/markitdown-cosense
8
+ Project-URL: Repository, https://github.com/kazu728/markitdown-cosense
9
+ Project-URL: Issues, https://github.com/kazu728/markitdown-cosense/issues
10
+ Keywords: markitdown,scrapbox,markdown,converter,plugin
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: markitdown>=0.1.1
15
+ Dynamic: license-file
16
+
17
+ # markitdown-cosense
18
+
19
+ A [markitdown](https://github.com/microsoft/markitdown) plugin for converting Scrapbox notation to Markdown.
20
+
21
+ ## Features
22
+
23
+ This plugin converts various Scrapbox notations to standard Markdown format:
24
+
25
+ - **Headings**: `[* Heading]` → `# Heading`
26
+ - **Text decorations**: `[/ italic]`, `[- strikethrough]`, `[** bold]`
27
+ - **Lists**: Indented lines with spaces, tabs, or full-width spaces
28
+ - **Code blocks**: `code:language` notation
29
+ - **Tables**: `table:name` notation
30
+ - **Links and images**: `[title url]`, `[img url]`
31
+ - **Math expressions**: `[$ formula $]` → `$formula$`
32
+ - **LaTeX blocks**: `code:tex` with mathematical content
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install markitdown-cosense
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ### With markitdown CLI
43
+
44
+ ```bash
45
+ # List available plugins
46
+ markitdown --list-plugins
47
+
48
+ # Convert a file using the plugin
49
+ markitdown --use-plugins your-file.txt
50
+ ```
51
+
52
+ ### Programmatic usage
53
+
54
+ ```python
55
+ from markitdown import MarkItDown
56
+ from markitdown_cosense import register_converters
57
+
58
+ # Initialize markitdown
59
+ md = MarkItDown()
60
+
61
+ # Register the converter
62
+ register_converters(md)
63
+
64
+ # Convert a file
65
+ result = md.convert("your-scrapbox-file.txt")
66
+ print(result.text_content)
67
+ ```
68
+
69
+ ## Features
70
+
71
+ - Converts Scrapbox notation to standard Markdown
72
+ - Handles headings, formatting, links, images, lists, tables, code blocks, and math notation
73
+ - Converts tags `[tag]` to HTML comments `<!-- tag: tag -->` to preserve information while maintaining valid Markdown
74
+
75
+ ## Examples
76
+
77
+ ### Input (Scrapbox notation)
78
+
79
+ ```
80
+ [* Project Title]
81
+
82
+ [** Overview]
83
+ This project is about [/ converting] Scrapbox notation.
84
+
85
+ Features:
86
+ Main feature
87
+ Sub feature 1
88
+ Sub feature 2
89
+ Another feature
90
+
91
+ code:python
92
+ def hello():
93
+ print("Hello, World!")
94
+
95
+ table:Results
96
+ Name Score Grade
97
+ Alice 95 A
98
+ Bob 87 B
99
+ ```
100
+
101
+ ### Output (Markdown)
102
+
103
+ ```markdown
104
+ # Project Title
105
+
106
+ ## Overview
107
+ This project is about *converting* Scrapbox notation.
108
+
109
+ Features:
110
+ - Main feature
111
+ - Sub feature 1
112
+ - Sub feature 2
113
+ - Another feature
114
+
115
+ ```python
116
+ def hello():
117
+ print("Hello, World!")
118
+ ```
119
+
120
+ ## Results
121
+
122
+ | Name | Score | Grade |
123
+ |---|---|---|
124
+ | Alice | 95 | A |
125
+ | Bob | 87 | B |
126
+ ```
@@ -0,0 +1,8 @@
1
+ markitdown_cosense/__init__.py,sha256=3-ahVQiE9P8HHnSTZByeATTG-r9oERkMCwAELhGfjTQ,274
2
+ markitdown_cosense/_plugin.py,sha256=4Wvp6FXglHF2Xn9BlocLlaWgAykVzmezGp_A3DJRDLY,10900
3
+ markitdown_cosense-0.1.0.dist-info/licenses/LICENSE,sha256=D63V7wYVic1bwmq79kCxL9okn7HdlaoJ0too_vJ8LL0,1063
4
+ markitdown_cosense-0.1.0.dist-info/METADATA,sha256=_G2jHlQuIrDNJVd1TwOiD2DiF1mKKf4wuGFgpYI6N_A,2752
5
+ markitdown_cosense-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ markitdown_cosense-0.1.0.dist-info/entry_points.txt,sha256=BkLrFPlYeHeXuykl3pclx4xywFwCgUK-nCSu8VF121I,60
7
+ markitdown_cosense-0.1.0.dist-info/top_level.txt,sha256=rs3Ha_XfbY6RemjYc4Dq_93pbLFgk8wJ7Z-YH-7r-FY,19
8
+ markitdown_cosense-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [markitdown.plugin]
2
+ markitdown-cosense = markitdown_cosense
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 kazu728
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.
@@ -0,0 +1 @@
1
+ markitdown_cosense