mdship 0.0.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.
- mdship-0.0.0/.gitignore +27 -0
- mdship-0.0.0/.idea/.gitignore +3 -0
- mdship-0.0.0/.idea/encodings.xml +4 -0
- mdship-0.0.0/.idea/inspectionProfiles/Project_Default.xml +13 -0
- mdship-0.0.0/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- mdship-0.0.0/.idea/mdship.iml +14 -0
- mdship-0.0.0/.idea/misc.xml +7 -0
- mdship-0.0.0/.idea/modules.xml +8 -0
- mdship-0.0.0/.idea/workspace.xml +47 -0
- mdship-0.0.0/.mcp.json +10 -0
- mdship-0.0.0/CLAUDE.md +116 -0
- mdship-0.0.0/PKG-INFO +11 -0
- mdship-0.0.0/README.md +65 -0
- mdship-0.0.0/build.sh +17 -0
- mdship-0.0.0/mdship/__init__.py +3 -0
- mdship-0.0.0/mdship/cli.py +106 -0
- mdship-0.0.0/mdship/markdown.py +179 -0
- mdship-0.0.0/mdship/mcp_server.py +133 -0
- mdship-0.0.0/pyproject.toml +36 -0
- mdship-0.0.0/release.sh +14 -0
- mdship-0.0.0/tests/__init__.py +0 -0
- mdship-0.0.0/tests/test_markdown.py +77 -0
- mdship-0.0.0/uv.lock +875 -0
mdship-0.0.0/.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
.Python
|
|
6
|
+
build/
|
|
7
|
+
develop-eggs/
|
|
8
|
+
dist/
|
|
9
|
+
downloads/
|
|
10
|
+
eggs/
|
|
11
|
+
.eggs/
|
|
12
|
+
lib/
|
|
13
|
+
lib64/
|
|
14
|
+
parts/
|
|
15
|
+
sdist/
|
|
16
|
+
var/
|
|
17
|
+
wheels/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
.installed.cfg
|
|
20
|
+
*.egg
|
|
21
|
+
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
env/
|
|
27
|
+
.DS_Store
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<component name="InspectionProjectProfileManager">
|
|
2
|
+
<profile version="1.0">
|
|
3
|
+
<option name="myName" value="Project Default" />
|
|
4
|
+
<inspection_tool class="PyStubPackagesAdvertiser" enabled="true" level="WARNING" enabled_by_default="true">
|
|
5
|
+
<option name="ignoredPackages">
|
|
6
|
+
<list>
|
|
7
|
+
<option value="pyspark-stubs==3.0.0.dev8" />
|
|
8
|
+
</list>
|
|
9
|
+
</option>
|
|
10
|
+
</inspection_tool>
|
|
11
|
+
<inspection_tool class="ReassignedToPlainText" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
12
|
+
</profile>
|
|
13
|
+
</component>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="PYTHON_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
|
6
|
+
</content>
|
|
7
|
+
<orderEntry type="jdk" jdkName="uv (mdship)" jdkType="Python SDK" />
|
|
8
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
9
|
+
</component>
|
|
10
|
+
<component name="PyDocumentationSettings">
|
|
11
|
+
<option name="format" value="PLAIN" />
|
|
12
|
+
<option name="myDocStringFormat" value="Plain" />
|
|
13
|
+
</component>
|
|
14
|
+
</module>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="Black">
|
|
4
|
+
<option name="sdkName" value="uv (mdship)" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="ProjectRootManager" version="2" project-jdk-name="uv (mdship)" project-jdk-type="Python SDK" />
|
|
7
|
+
</project>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="AutoImportSettings">
|
|
4
|
+
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="ChangeListManager">
|
|
7
|
+
<list default="true" id="57e4e07a-8725-42a0-b41e-c0333c53c14c" name="Changes" comment="" />
|
|
8
|
+
<option name="SHOW_DIALOG" value="false" />
|
|
9
|
+
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
10
|
+
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
11
|
+
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
12
|
+
</component>
|
|
13
|
+
<component name="ProjectColorInfo"><![CDATA[{
|
|
14
|
+
"associatedIndex": 4
|
|
15
|
+
}]]></component>
|
|
16
|
+
<component name="ProjectId" id="3EgA9ad6RYxNiPjwWPDgNaiD5hf" />
|
|
17
|
+
<component name="ProjectViewState">
|
|
18
|
+
<option name="hideEmptyMiddlePackages" value="true" />
|
|
19
|
+
<option name="showLibraryContents" value="true" />
|
|
20
|
+
</component>
|
|
21
|
+
<component name="PropertiesComponent"><![CDATA[{
|
|
22
|
+
"keyToString": {
|
|
23
|
+
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
|
24
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
25
|
+
"junie.onboarding.icon.badge.shown": "true",
|
|
26
|
+
"last_opened_file_path": "/Users/verhasp/github/mdship",
|
|
27
|
+
"settings.editor.selected.configurable": "preferences.pluginManager"
|
|
28
|
+
}
|
|
29
|
+
}]]></component>
|
|
30
|
+
<component name="SharedIndexes">
|
|
31
|
+
<attachedChunks>
|
|
32
|
+
<set>
|
|
33
|
+
<option value="bundled-python-sdk-9f8e2b94138c-36ea0e71a18c-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-251.26094.141" />
|
|
34
|
+
</set>
|
|
35
|
+
</attachedChunks>
|
|
36
|
+
</component>
|
|
37
|
+
<component name="TaskManager">
|
|
38
|
+
<task active="true" id="Default" summary="Default task">
|
|
39
|
+
<changelist id="57e4e07a-8725-42a0-b41e-c0333c53c14c" name="Changes" comment="" />
|
|
40
|
+
<created>1780589687264</created>
|
|
41
|
+
<option name="number" value="Default" />
|
|
42
|
+
<option name="presentableId" value="Default" />
|
|
43
|
+
<updated>1780589687264</updated>
|
|
44
|
+
</task>
|
|
45
|
+
<servers />
|
|
46
|
+
</component>
|
|
47
|
+
</project>
|
mdship-0.0.0/.mcp.json
ADDED
mdship-0.0.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# mdship — Markdown Manipulation Tool
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
**mdship** is a command-line tool and MCP (Model Context Protocol) server for manipulating markdown files. It provides utilities for fixing heading hierarchies, shifting heading levels, adding content checksums, and reflowing paragraphs.
|
|
6
|
+
|
|
7
|
+
The tool runs locally with no external dependencies (except for the base Python environment) and can be invoked either as a CLI command or as an MCP server for use with Claude.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Repository Structure
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
mdship/
|
|
15
|
+
├── CLAUDE.md # This file
|
|
16
|
+
├── README.md # User-facing documentation
|
|
17
|
+
├── pyproject.toml # Project definition and dependencies
|
|
18
|
+
├── .mcp.json # MCP server configuration
|
|
19
|
+
├── .gitignore
|
|
20
|
+
│
|
|
21
|
+
├── mdship/ # Main package
|
|
22
|
+
│ ├── __init__.py
|
|
23
|
+
│ ├── cli.py # CLI command dispatcher (typer app)
|
|
24
|
+
│ ├── markdown.py # Core markdown manipulation functions
|
|
25
|
+
│ └── mcp_server.py # MCP server implementation
|
|
26
|
+
│
|
|
27
|
+
└── tests/ # Test suite
|
|
28
|
+
├── __init__.py
|
|
29
|
+
└── test_markdown.py # Unit tests for markdown functions
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Core Functions
|
|
35
|
+
|
|
36
|
+
### `fix_heading_levels(content: str) -> str`
|
|
37
|
+
|
|
38
|
+
Ensures proper heading hierarchy. Should prevent skipping levels (e.g., h1 → h3).
|
|
39
|
+
|
|
40
|
+
**Status**: Skeleton only, needs implementation
|
|
41
|
+
|
|
42
|
+
### `shift_heading_levels(content: str, levels: int) -> str`
|
|
43
|
+
|
|
44
|
+
Shifts all headings by N levels. Positive = lower (h1 → h2), negative = raise (h2 → h1).
|
|
45
|
+
|
|
46
|
+
**Status**: Basic implementation complete
|
|
47
|
+
|
|
48
|
+
### `add_content_checksum(content: str, algorithm: str) -> str`
|
|
49
|
+
|
|
50
|
+
Adds or updates a checksum in YAML front-matter. Supports md5, sha1, sha256.
|
|
51
|
+
|
|
52
|
+
**Status**: Basic implementation complete
|
|
53
|
+
|
|
54
|
+
### `reflow_paragraphs(content: str, width: Optional[int]) -> str`
|
|
55
|
+
|
|
56
|
+
Reflows paragraphs to a specified width, or one sentence per line if width=0.
|
|
57
|
+
|
|
58
|
+
**Status**: Basic implementation complete
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## CLI Interface
|
|
63
|
+
|
|
64
|
+
Commands are dispatched via `typer.Typer` in `cli.py`. Each command:
|
|
65
|
+
- Takes a markdown file as an argument
|
|
66
|
+
- Outputs the result to stdout
|
|
67
|
+
- Exits with code 1 on error
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
mdship fix-headings file.md
|
|
71
|
+
mdship shift-headings file.md --levels 1
|
|
72
|
+
mdship add-checksum file.md --algorithm sha256
|
|
73
|
+
mdship reflow file.md --width 80
|
|
74
|
+
mdship mcp # Start MCP server on stdio
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## MCP Integration
|
|
80
|
+
|
|
81
|
+
The `mcp_server.py` module implements a stdio-based MCP server that exposes the same markdown functions as async tools. The server:
|
|
82
|
+
|
|
83
|
+
- Runs on stdin/stdout only (no network)
|
|
84
|
+
- Exposes tools: `fix_headings`, `shift_headings`, `add_checksum`, `reflow`
|
|
85
|
+
- Handles errors gracefully and returns error messages as text content
|
|
86
|
+
|
|
87
|
+
Configure in Claude's MCP settings:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"mdship": {
|
|
93
|
+
"command": "mdship",
|
|
94
|
+
"args": ["mcp"]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Development Notes
|
|
103
|
+
|
|
104
|
+
- Uses `typer` for CLI, `mcp` Python SDK for server
|
|
105
|
+
- No external markdown parsing library yet; uses regex for headings
|
|
106
|
+
- Front-matter assumed to be YAML between `---` delimiters
|
|
107
|
+
- Code blocks (triple backticks) are preserved during reflow
|
|
108
|
+
- Tests use `pytest`; use `pytest -v` for detailed output
|
|
109
|
+
|
|
110
|
+
### Next Steps
|
|
111
|
+
|
|
112
|
+
1. Enhance `fix_heading_levels` to properly normalize hierarchies
|
|
113
|
+
2. Add more comprehensive markdown parsing (consider `markdown-it-py` or similar)
|
|
114
|
+
3. Add support for different markdown flavors (GFM, CommonMark, etc.)
|
|
115
|
+
4. Add recursive directory processing option
|
|
116
|
+
5. Add dry-run mode to preview changes
|
mdship-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mdship
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: CLI and MCP tool for manipulating markdown files
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: mcp>=1.0
|
|
7
|
+
Requires-Dist: rich>=13
|
|
8
|
+
Requires-Dist: typer>=0.12
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
11
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
mdship-0.0.0/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# mdship
|
|
2
|
+
|
|
3
|
+
A command-line and MCP tool for manipulating markdown files.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Fix heading levels**: Ensure consistent heading hierarchy
|
|
8
|
+
- **Shift headings**: Move all headings up or down by N levels
|
|
9
|
+
- **Add checksums**: Insert content checksums into front-matter
|
|
10
|
+
- **Reflow paragraphs**: Reflow text to a specific width or one sentence per line
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv sync
|
|
16
|
+
uv run pip install -e .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Command Line
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
mdship fix-headings file.md
|
|
25
|
+
mdship shift-headings file.md --levels 1
|
|
26
|
+
mdship add-checksum file.md --algorithm sha256
|
|
27
|
+
mdship reflow file.md --width 80
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### MCP Server
|
|
31
|
+
|
|
32
|
+
Configure in your Claude settings:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"mdship": {
|
|
38
|
+
"command": "mdship",
|
|
39
|
+
"args": ["mcp"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then use the available tools in Claude with markdown content.
|
|
46
|
+
|
|
47
|
+
## Development
|
|
48
|
+
|
|
49
|
+
Install dependencies:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv sync
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Run tests:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pytest
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Format code:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
ruff check --fix
|
|
65
|
+
```
|
mdship-0.0.0/build.sh
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
cd "$SCRIPT_DIR"
|
|
6
|
+
|
|
7
|
+
echo "=== Syncing dependencies ==="
|
|
8
|
+
uv sync --all-extras --python /opt/homebrew/bin/python3
|
|
9
|
+
|
|
10
|
+
echo "=== Running tests ==="
|
|
11
|
+
uv run pytest tests/ -v
|
|
12
|
+
|
|
13
|
+
echo "=== Building distribution ==="
|
|
14
|
+
uv build
|
|
15
|
+
|
|
16
|
+
VERSION=$(uv run python -c "from importlib.metadata import version; print(version('mdship'))")
|
|
17
|
+
echo "=== Built mdship $VERSION ==="
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from importlib.metadata import version as _pkg_version
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
_VERSION = _pkg_version("mdship")
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(
|
|
12
|
+
help=f"mdship — markdown manipulation tool (version {_VERSION})",
|
|
13
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
14
|
+
)
|
|
15
|
+
err = Console(stderr=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _version_callback(value: bool) -> None:
|
|
19
|
+
if value:
|
|
20
|
+
print(f"mdship {_VERSION}")
|
|
21
|
+
raise typer.Exit()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.callback()
|
|
25
|
+
def _main(
|
|
26
|
+
_: Annotated[
|
|
27
|
+
bool | None,
|
|
28
|
+
typer.Option("--version", "-V", callback=_version_callback, is_eager=True, help="Show version and exit."),
|
|
29
|
+
] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command()
|
|
35
|
+
def fix_headings(
|
|
36
|
+
file: Annotated[Path, typer.Argument(help="Markdown file to process")],
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Fix heading levels to ensure consistent hierarchy."""
|
|
39
|
+
if not file.exists():
|
|
40
|
+
err.print(f"[red]Error:[/red] file not found: {file}")
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
|
|
43
|
+
from mdship.markdown import fix_heading_levels
|
|
44
|
+
|
|
45
|
+
content = file.read_text()
|
|
46
|
+
fixed_content = fix_heading_levels(content)
|
|
47
|
+
print(fixed_content, end="")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command()
|
|
51
|
+
def shift_headings(
|
|
52
|
+
file: Annotated[Path, typer.Argument(help="Markdown file to process")],
|
|
53
|
+
levels: Annotated[int, typer.Option("--levels", "-l", help="Number of levels to shift (positive=lower, negative=higher)")] = 1,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Shift all headings by the specified number of levels."""
|
|
56
|
+
if not file.exists():
|
|
57
|
+
err.print(f"[red]Error:[/red] file not found: {file}")
|
|
58
|
+
raise typer.Exit(1)
|
|
59
|
+
|
|
60
|
+
from mdship.markdown import shift_heading_levels
|
|
61
|
+
|
|
62
|
+
content = file.read_text()
|
|
63
|
+
shifted_content = shift_heading_levels(content, levels)
|
|
64
|
+
print(shifted_content, end="")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command()
|
|
68
|
+
def add_checksum(
|
|
69
|
+
file: Annotated[Path, typer.Argument(help="Markdown file to process")],
|
|
70
|
+
algorithm: Annotated[str, typer.Option("--algorithm", "-a", help="Hash algorithm (md5, sha256, sha1)")] = "sha256",
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Add or update checksum in front-matter."""
|
|
73
|
+
if not file.exists():
|
|
74
|
+
err.print(f"[red]Error:[/red] file not found: {file}")
|
|
75
|
+
raise typer.Exit(1)
|
|
76
|
+
|
|
77
|
+
from mdship.markdown import add_content_checksum
|
|
78
|
+
|
|
79
|
+
content = file.read_text()
|
|
80
|
+
updated_content = add_content_checksum(content, algorithm)
|
|
81
|
+
print(updated_content, end="")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command()
|
|
85
|
+
def reflow(
|
|
86
|
+
file: Annotated[Path, typer.Argument(help="Markdown file to process")],
|
|
87
|
+
width: Annotated[int | None, typer.Option("--width", "-w", help="Line width (0 for one sentence per line)")] = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Reflow paragraphs to specified width or one sentence per line."""
|
|
90
|
+
if not file.exists():
|
|
91
|
+
err.print(f"[red]Error:[/red] file not found: {file}")
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
|
|
94
|
+
from mdship.markdown import reflow_paragraphs
|
|
95
|
+
|
|
96
|
+
content = file.read_text()
|
|
97
|
+
reflowed_content = reflow_paragraphs(content, width)
|
|
98
|
+
print(reflowed_content, end="")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command()
|
|
102
|
+
def mcp() -> None:
|
|
103
|
+
"""Start mdship as an MCP server on stdio."""
|
|
104
|
+
from mdship.mcp_server import main as mcp_main
|
|
105
|
+
|
|
106
|
+
mcp_main()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Core markdown manipulation functions."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def fix_heading_levels(content: str) -> str:
|
|
9
|
+
"""Fix heading levels to ensure consistent hierarchy.
|
|
10
|
+
|
|
11
|
+
Ensures headings follow proper nesting (no skipping from h1 to h3, etc).
|
|
12
|
+
"""
|
|
13
|
+
lines = content.split("\n")
|
|
14
|
+
result = []
|
|
15
|
+
min_level = None
|
|
16
|
+
|
|
17
|
+
for line in lines:
|
|
18
|
+
match = re.match(r"^(#{1,6})\s+(.+)$", line)
|
|
19
|
+
if match:
|
|
20
|
+
level = len(match.group(1))
|
|
21
|
+
text = match.group(2)
|
|
22
|
+
|
|
23
|
+
if min_level is None:
|
|
24
|
+
min_level = level
|
|
25
|
+
else:
|
|
26
|
+
# TODO: Implement logic to adjust heading levels
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
result.append(line)
|
|
30
|
+
else:
|
|
31
|
+
result.append(line)
|
|
32
|
+
|
|
33
|
+
return "\n".join(result)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def shift_heading_levels(content: str, levels: int) -> str:
|
|
37
|
+
"""Shift all headings by the specified number of levels.
|
|
38
|
+
|
|
39
|
+
Positive numbers lower the headings (h1 -> h2), negative numbers raise them (h2 -> h1).
|
|
40
|
+
"""
|
|
41
|
+
lines = content.split("\n")
|
|
42
|
+
result = []
|
|
43
|
+
|
|
44
|
+
for line in lines:
|
|
45
|
+
match = re.match(r"^(#{1,6})\s+(.+)$", line)
|
|
46
|
+
if match:
|
|
47
|
+
current_level = len(match.group(1))
|
|
48
|
+
text = match.group(2)
|
|
49
|
+
new_level = max(1, min(6, current_level + levels))
|
|
50
|
+
result.append("#" * new_level + " " + text)
|
|
51
|
+
else:
|
|
52
|
+
result.append(line)
|
|
53
|
+
|
|
54
|
+
return "\n".join(result)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def add_content_checksum(content: str, algorithm: str = "sha256") -> str:
|
|
58
|
+
"""Add or update checksum in front-matter.
|
|
59
|
+
|
|
60
|
+
Supports md5, sha1, and sha256 algorithms.
|
|
61
|
+
"""
|
|
62
|
+
lines = content.split("\n")
|
|
63
|
+
|
|
64
|
+
if not lines or lines[0] != "---":
|
|
65
|
+
# No YAML front-matter, prepend it
|
|
66
|
+
hash_obj = hashlib.new(algorithm)
|
|
67
|
+
hash_obj.update(content.encode())
|
|
68
|
+
checksum = hash_obj.hexdigest()
|
|
69
|
+
front_matter = f"---\nchecksum: {checksum}\nchecksum_algorithm: {algorithm}\n---\n"
|
|
70
|
+
return front_matter + content
|
|
71
|
+
|
|
72
|
+
# Find the closing --- of front-matter
|
|
73
|
+
end_idx = None
|
|
74
|
+
for i in range(1, len(lines)):
|
|
75
|
+
if lines[i] == "---":
|
|
76
|
+
end_idx = i
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
if end_idx is None:
|
|
80
|
+
# Malformed front-matter, just add at the beginning
|
|
81
|
+
hash_obj = hashlib.new(algorithm)
|
|
82
|
+
hash_obj.update(content.encode())
|
|
83
|
+
checksum = hash_obj.hexdigest()
|
|
84
|
+
front_matter = f"---\nchecksum: {checksum}\nchecksum_algorithm: {algorithm}\n---\n"
|
|
85
|
+
return front_matter + content
|
|
86
|
+
|
|
87
|
+
# Calculate checksum of the content (excluding front-matter)
|
|
88
|
+
content_without_fm = "\n".join(lines[end_idx + 1 :])
|
|
89
|
+
hash_obj = hashlib.new(algorithm)
|
|
90
|
+
hash_obj.update(content_without_fm.encode())
|
|
91
|
+
checksum = hash_obj.hexdigest()
|
|
92
|
+
|
|
93
|
+
# Update or add checksum fields in front-matter
|
|
94
|
+
fm_lines = lines[1:end_idx]
|
|
95
|
+
checksum_line = f"checksum: {checksum}"
|
|
96
|
+
algorithm_line = f"checksum_algorithm: {algorithm}"
|
|
97
|
+
|
|
98
|
+
# Remove existing checksum lines
|
|
99
|
+
fm_lines = [
|
|
100
|
+
line
|
|
101
|
+
for line in fm_lines
|
|
102
|
+
if not line.startswith("checksum:") and not line.startswith("checksum_algorithm:")
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
# Add new checksum lines
|
|
106
|
+
fm_lines.append(checksum_line)
|
|
107
|
+
fm_lines.append(algorithm_line)
|
|
108
|
+
|
|
109
|
+
result = ["---"] + fm_lines + ["---"] + lines[end_idx + 1 :]
|
|
110
|
+
return "\n".join(result)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def reflow_paragraphs(content: str, width: Optional[int] = None) -> str:
|
|
114
|
+
"""Reflow paragraphs to specified width or one sentence per line.
|
|
115
|
+
|
|
116
|
+
If width is None or 0, splits into one sentence per line.
|
|
117
|
+
Otherwise, reflows to the specified width.
|
|
118
|
+
"""
|
|
119
|
+
lines = content.split("\n")
|
|
120
|
+
result = []
|
|
121
|
+
in_code_block = False
|
|
122
|
+
current_paragraph = []
|
|
123
|
+
|
|
124
|
+
for line in lines:
|
|
125
|
+
# Track code blocks
|
|
126
|
+
if line.startswith("```"):
|
|
127
|
+
in_code_block = not in_code_block
|
|
128
|
+
|
|
129
|
+
if in_code_block:
|
|
130
|
+
if current_paragraph:
|
|
131
|
+
result.extend(_reflow_paragraph(current_paragraph, width))
|
|
132
|
+
current_paragraph = []
|
|
133
|
+
result.append(line)
|
|
134
|
+
elif line.strip() == "":
|
|
135
|
+
# Empty line marks end of paragraph
|
|
136
|
+
if current_paragraph:
|
|
137
|
+
result.extend(_reflow_paragraph(current_paragraph, width))
|
|
138
|
+
current_paragraph = []
|
|
139
|
+
result.append("")
|
|
140
|
+
elif re.match(r"^[#\-\*]", line) or line.startswith(">"):
|
|
141
|
+
# Heading, list, or blockquote
|
|
142
|
+
if current_paragraph:
|
|
143
|
+
result.extend(_reflow_paragraph(current_paragraph, width))
|
|
144
|
+
current_paragraph = []
|
|
145
|
+
result.append(line)
|
|
146
|
+
else:
|
|
147
|
+
# Regular paragraph line
|
|
148
|
+
current_paragraph.append(line)
|
|
149
|
+
|
|
150
|
+
# Handle remaining paragraph
|
|
151
|
+
if current_paragraph:
|
|
152
|
+
result.extend(_reflow_paragraph(current_paragraph, width))
|
|
153
|
+
|
|
154
|
+
return "\n".join(result)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _reflow_paragraph(lines: list[str], width: Optional[int] = None) -> list[str]:
|
|
158
|
+
"""Reflow a paragraph (list of lines) to the specified width or sentence per line."""
|
|
159
|
+
text = " ".join(line.strip() for line in lines if line.strip())
|
|
160
|
+
|
|
161
|
+
if width is None or width == 0:
|
|
162
|
+
# One sentence per line
|
|
163
|
+
sentences = re.split(r"(?<=[.!?])\s+", text)
|
|
164
|
+
return [s.strip() for s in sentences if s.strip()]
|
|
165
|
+
else:
|
|
166
|
+
# Reflow to width
|
|
167
|
+
result = []
|
|
168
|
+
current_line = ""
|
|
169
|
+
for word in text.split():
|
|
170
|
+
if not current_line:
|
|
171
|
+
current_line = word
|
|
172
|
+
elif len(current_line) + 1 + len(word) <= width:
|
|
173
|
+
current_line += " " + word
|
|
174
|
+
else:
|
|
175
|
+
result.append(current_line)
|
|
176
|
+
current_line = word
|
|
177
|
+
if current_line:
|
|
178
|
+
result.append(current_line)
|
|
179
|
+
return result
|