viewmd 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.
- viewmd-0.1.0/LICENSE +21 -0
- viewmd-0.1.0/MANIFEST.in +3 -0
- viewmd-0.1.0/PKG-INFO +114 -0
- viewmd-0.1.0/README.md +89 -0
- viewmd-0.1.0/pyproject.toml +37 -0
- viewmd-0.1.0/requirements.txt +1 -0
- viewmd-0.1.0/setup.cfg +4 -0
- viewmd-0.1.0/viewmd.egg-info/PKG-INFO +114 -0
- viewmd-0.1.0/viewmd.egg-info/SOURCES.txt +12 -0
- viewmd-0.1.0/viewmd.egg-info/dependency_links.txt +1 -0
- viewmd-0.1.0/viewmd.egg-info/entry_points.txt +2 -0
- viewmd-0.1.0/viewmd.egg-info/requires.txt +1 -0
- viewmd-0.1.0/viewmd.egg-info/top_level.txt +1 -0
- viewmd-0.1.0/viewmd.py +361 -0
viewmd-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 German Greiner
|
|
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.
|
viewmd-0.1.0/MANIFEST.in
ADDED
viewmd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: viewmd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple HTTP server for viewing Markdown files in your browser
|
|
5
|
+
Author: German Greiner
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/driangle/viewmd
|
|
8
|
+
Keywords: markdown,viewer,http-server,cli
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.7
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: markdown>=3.0
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# viewmd
|
|
27
|
+
|
|
28
|
+
A simple HTTP server for viewing Markdown files in your browser.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
### Option 1: Using pipx (Recommended)
|
|
33
|
+
|
|
34
|
+
[pipx](https://pypa.github.io/pipx/) installs CLI tools in isolated environments, preventing dependency conflicts:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Install pipx if you don't have it
|
|
38
|
+
python3 -m pip install --user pipx
|
|
39
|
+
python3 -m pipx ensurepath
|
|
40
|
+
|
|
41
|
+
# Install viewmd
|
|
42
|
+
pipx install .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Option 2: Using pip
|
|
46
|
+
|
|
47
|
+
Install globally with pip:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or install in development mode (changes to source code take effect immediately):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install -e .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Option 3: Direct symlink (Manual)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
chmod +x viewmd.py
|
|
63
|
+
sudo ln -s "$(pwd)/viewmd.py" /usr/local/bin/viewmd
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Run from any directory
|
|
70
|
+
viewmd
|
|
71
|
+
|
|
72
|
+
# Open http://localhost:8000
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## What It Does
|
|
76
|
+
|
|
77
|
+
- **Markdown files** (`.md`) - Rendered as HTML with nice styling
|
|
78
|
+
- **Text files** (`.py`, `.json`, `.gitignore`, etc.) - Displayed in browser
|
|
79
|
+
- **Directories** - Shows file listing, auto-displays `README.md`
|
|
80
|
+
- **Other files** - Served normally (images, PDFs, etc.)
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
viewmd # Starts on port 8000
|
|
86
|
+
viewmd 3000 # Custom port
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Uninstallation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# If installed with pipx
|
|
93
|
+
pipx uninstall viewmd
|
|
94
|
+
|
|
95
|
+
# If installed with pip
|
|
96
|
+
pip uninstall viewmd
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Publishing to PyPI (Optional)
|
|
100
|
+
|
|
101
|
+
To make viewmd installable via `pip install viewmd` for everyone:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Install build tools
|
|
105
|
+
pip install build twine
|
|
106
|
+
|
|
107
|
+
# Build the package
|
|
108
|
+
python -m build
|
|
109
|
+
|
|
110
|
+
# Upload to PyPI (requires account at pypi.org)
|
|
111
|
+
python -m twine upload dist/*
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
That's it. Simple.
|
viewmd-0.1.0/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# viewmd
|
|
2
|
+
|
|
3
|
+
A simple HTTP server for viewing Markdown files in your browser.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Option 1: Using pipx (Recommended)
|
|
8
|
+
|
|
9
|
+
[pipx](https://pypa.github.io/pipx/) installs CLI tools in isolated environments, preventing dependency conflicts:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install pipx if you don't have it
|
|
13
|
+
python3 -m pip install --user pipx
|
|
14
|
+
python3 -m pipx ensurepath
|
|
15
|
+
|
|
16
|
+
# Install viewmd
|
|
17
|
+
pipx install .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Option 2: Using pip
|
|
21
|
+
|
|
22
|
+
Install globally with pip:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install in development mode (changes to source code take effect immediately):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Option 3: Direct symlink (Manual)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
chmod +x viewmd.py
|
|
38
|
+
sudo ln -s "$(pwd)/viewmd.py" /usr/local/bin/viewmd
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Run from any directory
|
|
45
|
+
viewmd
|
|
46
|
+
|
|
47
|
+
# Open http://localhost:8000
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## What It Does
|
|
51
|
+
|
|
52
|
+
- **Markdown files** (`.md`) - Rendered as HTML with nice styling
|
|
53
|
+
- **Text files** (`.py`, `.json`, `.gitignore`, etc.) - Displayed in browser
|
|
54
|
+
- **Directories** - Shows file listing, auto-displays `README.md`
|
|
55
|
+
- **Other files** - Served normally (images, PDFs, etc.)
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
viewmd # Starts on port 8000
|
|
61
|
+
viewmd 3000 # Custom port
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Uninstallation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# If installed with pipx
|
|
68
|
+
pipx uninstall viewmd
|
|
69
|
+
|
|
70
|
+
# If installed with pip
|
|
71
|
+
pip uninstall viewmd
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Publishing to PyPI (Optional)
|
|
75
|
+
|
|
76
|
+
To make viewmd installable via `pip install viewmd` for everyone:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Install build tools
|
|
80
|
+
pip install build twine
|
|
81
|
+
|
|
82
|
+
# Build the package
|
|
83
|
+
python -m build
|
|
84
|
+
|
|
85
|
+
# Upload to PyPI (requires account at pypi.org)
|
|
86
|
+
python -m twine upload dist/*
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
That's it. Simple.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=45", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "viewmd"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Simple HTTP server for viewing Markdown files in your browser"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.7"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "German Greiner"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["markdown", "viewer", "http-server", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.7",
|
|
23
|
+
"Programming Language :: Python :: 3.8",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Topic :: Utilities",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"markdown>=3.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
viewmd = "viewmd:main"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/driangle/viewmd"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
markdown>=3.0
|
viewmd-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: viewmd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple HTTP server for viewing Markdown files in your browser
|
|
5
|
+
Author: German Greiner
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/driangle/viewmd
|
|
8
|
+
Keywords: markdown,viewer,http-server,cli
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.7
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: markdown>=3.0
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# viewmd
|
|
27
|
+
|
|
28
|
+
A simple HTTP server for viewing Markdown files in your browser.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
### Option 1: Using pipx (Recommended)
|
|
33
|
+
|
|
34
|
+
[pipx](https://pypa.github.io/pipx/) installs CLI tools in isolated environments, preventing dependency conflicts:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Install pipx if you don't have it
|
|
38
|
+
python3 -m pip install --user pipx
|
|
39
|
+
python3 -m pipx ensurepath
|
|
40
|
+
|
|
41
|
+
# Install viewmd
|
|
42
|
+
pipx install .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Option 2: Using pip
|
|
46
|
+
|
|
47
|
+
Install globally with pip:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or install in development mode (changes to source code take effect immediately):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install -e .
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Option 3: Direct symlink (Manual)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
chmod +x viewmd.py
|
|
63
|
+
sudo ln -s "$(pwd)/viewmd.py" /usr/local/bin/viewmd
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quick Start
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Run from any directory
|
|
70
|
+
viewmd
|
|
71
|
+
|
|
72
|
+
# Open http://localhost:8000
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## What It Does
|
|
76
|
+
|
|
77
|
+
- **Markdown files** (`.md`) - Rendered as HTML with nice styling
|
|
78
|
+
- **Text files** (`.py`, `.json`, `.gitignore`, etc.) - Displayed in browser
|
|
79
|
+
- **Directories** - Shows file listing, auto-displays `README.md`
|
|
80
|
+
- **Other files** - Served normally (images, PDFs, etc.)
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
viewmd # Starts on port 8000
|
|
86
|
+
viewmd 3000 # Custom port
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Uninstallation
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# If installed with pipx
|
|
93
|
+
pipx uninstall viewmd
|
|
94
|
+
|
|
95
|
+
# If installed with pip
|
|
96
|
+
pip uninstall viewmd
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Publishing to PyPI (Optional)
|
|
100
|
+
|
|
101
|
+
To make viewmd installable via `pip install viewmd` for everyone:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Install build tools
|
|
105
|
+
pip install build twine
|
|
106
|
+
|
|
107
|
+
# Build the package
|
|
108
|
+
python -m build
|
|
109
|
+
|
|
110
|
+
# Upload to PyPI (requires account at pypi.org)
|
|
111
|
+
python -m twine upload dist/*
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
That's it. Simple.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
requirements.txt
|
|
6
|
+
viewmd.py
|
|
7
|
+
viewmd.egg-info/PKG-INFO
|
|
8
|
+
viewmd.egg-info/SOURCES.txt
|
|
9
|
+
viewmd.egg-info/dependency_links.txt
|
|
10
|
+
viewmd.egg-info/entry_points.txt
|
|
11
|
+
viewmd.egg-info/requires.txt
|
|
12
|
+
viewmd.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
markdown>=3.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
viewmd
|
viewmd-0.1.0/viewmd.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
viewmd - Simple HTTP server for viewing Markdown files in your browser.
|
|
4
|
+
Usage: viewmd [port]
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import html
|
|
9
|
+
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import urllib.parse
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
VERSION = "0.1.0"
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import markdown
|
|
18
|
+
except ImportError:
|
|
19
|
+
print("Error: 'markdown' package not found.")
|
|
20
|
+
print("Install it with: pip3 install markdown")
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MarkdownHandler(SimpleHTTPRequestHandler):
|
|
25
|
+
# Text file extensions to display in browser
|
|
26
|
+
TEXT_EXTENSIONS = {
|
|
27
|
+
'.txt', '.log', '.json', '.xml', '.yaml', '.yml', '.toml', '.ini', '.cfg', '.conf',
|
|
28
|
+
'.sh', '.bash', '.zsh', '.fish', '.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.c',
|
|
29
|
+
'.cpp', '.h', '.hpp', '.cs', '.go', '.rs', '.rb', '.php', '.swift', '.kt', '.sql',
|
|
30
|
+
'.html', '.css', '.scss', '.sass', '.less', '.vue', '.svelte', '.r', '.m', '.scala',
|
|
31
|
+
'.pl', '.lua', '.vim', '.el', '.clj', '.ex', '.exs', '.dockerfile', '.env', '.gitignore',
|
|
32
|
+
'.gitattributes', '.editorconfig', '.eslintrc', '.prettierrc', '.babelrc'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Files without extensions that are typically text
|
|
36
|
+
TEXT_FILENAMES = {
|
|
37
|
+
'makefile', 'dockerfile', 'gemfile', 'rakefile', 'procfile', 'jenkinsfile',
|
|
38
|
+
'license', 'readme', 'changelog', 'authors', 'contributors', 'codeowners'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def do_GET(self):
|
|
42
|
+
# Parse the URL and remove query parameters
|
|
43
|
+
parsed_path = urllib.parse.urlparse(self.path)
|
|
44
|
+
path = parsed_path.path.lstrip('/')
|
|
45
|
+
|
|
46
|
+
# Decode URL encoding
|
|
47
|
+
path = urllib.parse.unquote(path)
|
|
48
|
+
|
|
49
|
+
print(f"[{datetime.now().strftime('%H:%M:%S')}] Request: {path or '/'}")
|
|
50
|
+
|
|
51
|
+
# Handle root directory
|
|
52
|
+
if not path:
|
|
53
|
+
print(f" → Serving directory listing")
|
|
54
|
+
self.serve_directory_listing()
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Get the file path
|
|
58
|
+
file_path = Path(path)
|
|
59
|
+
|
|
60
|
+
# Check if it's a directory
|
|
61
|
+
if file_path.is_dir():
|
|
62
|
+
# Check for README.md in the directory
|
|
63
|
+
readme_path = file_path / "README.md"
|
|
64
|
+
if readme_path.is_file():
|
|
65
|
+
print(f" → Rendering markdown: {readme_path}")
|
|
66
|
+
self.serve_markdown(readme_path)
|
|
67
|
+
else:
|
|
68
|
+
print(f" → Serving directory listing")
|
|
69
|
+
self.serve_directory_listing()
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Check if file exists
|
|
73
|
+
if not file_path.is_file():
|
|
74
|
+
print(f" → File not found, using default handler")
|
|
75
|
+
super().do_GET()
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# Check if it's a markdown file
|
|
79
|
+
if file_path.suffix.lower() in ['.md', '.markdown']:
|
|
80
|
+
print(f" → Rendering markdown: {file_path}")
|
|
81
|
+
self.serve_markdown(file_path)
|
|
82
|
+
# Check if it's a text file
|
|
83
|
+
elif self.is_text_file(file_path):
|
|
84
|
+
print(f" → Displaying text file: {file_path}")
|
|
85
|
+
self.serve_text_file(file_path)
|
|
86
|
+
else:
|
|
87
|
+
# Serve other files normally (images, PDFs, etc.)
|
|
88
|
+
print(f" → Serving binary file: {file_path}")
|
|
89
|
+
super().do_GET()
|
|
90
|
+
|
|
91
|
+
def is_text_file(self, file_path):
|
|
92
|
+
"""Check if a file should be displayed as text."""
|
|
93
|
+
filename = file_path.name
|
|
94
|
+
|
|
95
|
+
# Check extension
|
|
96
|
+
if file_path.suffix.lower() in self.TEXT_EXTENSIONS:
|
|
97
|
+
print(f" → Detected as text (extension: {file_path.suffix})")
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
# Check filename (for files without extensions)
|
|
101
|
+
if filename.lower() in self.TEXT_FILENAMES:
|
|
102
|
+
print(f" → Detected as text (known filename: {filename})")
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
# Check if filename starts with a dot (hidden config files like .gitignore, .env, etc.)
|
|
106
|
+
# In pathlib, .gitignore has suffix='.gitignore' and stem='', so we check if name starts with '.'
|
|
107
|
+
if filename.startswith('.'):
|
|
108
|
+
print(f" → Detected as text (dotfile: {filename})")
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
print(f" → Not detected as text file")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
def serve_markdown(self, file_path):
|
|
115
|
+
try:
|
|
116
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
117
|
+
content = f.read()
|
|
118
|
+
|
|
119
|
+
# Convert markdown to HTML
|
|
120
|
+
md = markdown.Markdown(extensions=['fenced_code', 'tables', 'nl2br'])
|
|
121
|
+
html_content = md.convert(content)
|
|
122
|
+
|
|
123
|
+
# Calculate the base URL for relative links
|
|
124
|
+
# This ensures links in markdown are relative to the file's directory
|
|
125
|
+
base_path = file_path.parent if file_path.parent != Path('.') else Path('/')
|
|
126
|
+
base_url = f"/{base_path}/" if str(base_path) != '.' else "/"
|
|
127
|
+
|
|
128
|
+
# Wrap in a nice HTML template
|
|
129
|
+
html = f"""<!DOCTYPE html>
|
|
130
|
+
<html>
|
|
131
|
+
<head>
|
|
132
|
+
<meta charset="utf-8">
|
|
133
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
134
|
+
<base href="{base_url}">
|
|
135
|
+
<title>{file_path.name}</title>
|
|
136
|
+
<style>
|
|
137
|
+
body {{
|
|
138
|
+
max-width: 800px;
|
|
139
|
+
margin: 40px auto;
|
|
140
|
+
padding: 0 20px;
|
|
141
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
142
|
+
line-height: 1.6;
|
|
143
|
+
color: #333;
|
|
144
|
+
}}
|
|
145
|
+
pre {{
|
|
146
|
+
background: #f4f4f4;
|
|
147
|
+
border: 1px solid #ddd;
|
|
148
|
+
border-radius: 4px;
|
|
149
|
+
padding: 12px;
|
|
150
|
+
overflow-x: auto;
|
|
151
|
+
}}
|
|
152
|
+
code {{
|
|
153
|
+
background: #f4f4f4;
|
|
154
|
+
padding: 2px 6px;
|
|
155
|
+
border-radius: 3px;
|
|
156
|
+
font-family: 'Courier New', monospace;
|
|
157
|
+
}}
|
|
158
|
+
pre code {{
|
|
159
|
+
background: none;
|
|
160
|
+
padding: 0;
|
|
161
|
+
}}
|
|
162
|
+
table {{
|
|
163
|
+
border-collapse: collapse;
|
|
164
|
+
width: 100%;
|
|
165
|
+
margin: 20px 0;
|
|
166
|
+
}}
|
|
167
|
+
th, td {{
|
|
168
|
+
border: 1px solid #ddd;
|
|
169
|
+
padding: 8px 12px;
|
|
170
|
+
text-align: left;
|
|
171
|
+
}}
|
|
172
|
+
th {{
|
|
173
|
+
background: #f4f4f4;
|
|
174
|
+
}}
|
|
175
|
+
a {{
|
|
176
|
+
color: #0066cc;
|
|
177
|
+
text-decoration: none;
|
|
178
|
+
}}
|
|
179
|
+
a:hover {{
|
|
180
|
+
text-decoration: underline;
|
|
181
|
+
}}
|
|
182
|
+
blockquote {{
|
|
183
|
+
border-left: 4px solid #ddd;
|
|
184
|
+
margin: 0;
|
|
185
|
+
padding-left: 20px;
|
|
186
|
+
color: #666;
|
|
187
|
+
}}
|
|
188
|
+
img {{
|
|
189
|
+
max-width: 100%;
|
|
190
|
+
height: auto;
|
|
191
|
+
}}
|
|
192
|
+
</style>
|
|
193
|
+
</head>
|
|
194
|
+
<body>
|
|
195
|
+
{html_content}
|
|
196
|
+
</body>
|
|
197
|
+
</html>"""
|
|
198
|
+
|
|
199
|
+
# Send response
|
|
200
|
+
self.send_response(200)
|
|
201
|
+
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|
202
|
+
self.end_headers()
|
|
203
|
+
self.wfile.write(html.encode('utf-8'))
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
self.send_error(500, f"Error rendering markdown: {str(e)}")
|
|
207
|
+
|
|
208
|
+
def serve_text_file(self, file_path):
|
|
209
|
+
"""Serve a text file with HTML formatting."""
|
|
210
|
+
try:
|
|
211
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
212
|
+
content = f.read()
|
|
213
|
+
|
|
214
|
+
# Escape HTML characters
|
|
215
|
+
escaped_content = html.escape(content)
|
|
216
|
+
|
|
217
|
+
# Wrap in HTML template
|
|
218
|
+
html_output = f"""<!DOCTYPE html>
|
|
219
|
+
<html>
|
|
220
|
+
<head>
|
|
221
|
+
<meta charset="utf-8">
|
|
222
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
223
|
+
<title>{file_path.name}</title>
|
|
224
|
+
<style>
|
|
225
|
+
body {{
|
|
226
|
+
max-width: 1000px;
|
|
227
|
+
margin: 20px auto;
|
|
228
|
+
padding: 0 20px;
|
|
229
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
230
|
+
background: #f6f8fa;
|
|
231
|
+
}}
|
|
232
|
+
.header {{
|
|
233
|
+
background: white;
|
|
234
|
+
border: 1px solid #d0d7de;
|
|
235
|
+
border-radius: 6px 6px 0 0;
|
|
236
|
+
padding: 12px 16px;
|
|
237
|
+
font-weight: 600;
|
|
238
|
+
border-bottom: 1px solid #d0d7de;
|
|
239
|
+
}}
|
|
240
|
+
.content {{
|
|
241
|
+
background: white;
|
|
242
|
+
border: 1px solid #d0d7de;
|
|
243
|
+
border-top: none;
|
|
244
|
+
border-radius: 0 0 6px 6px;
|
|
245
|
+
padding: 16px;
|
|
246
|
+
overflow-x: auto;
|
|
247
|
+
}}
|
|
248
|
+
pre {{
|
|
249
|
+
margin: 0;
|
|
250
|
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Courier New', monospace;
|
|
251
|
+
font-size: 12px;
|
|
252
|
+
line-height: 1.5;
|
|
253
|
+
white-space: pre;
|
|
254
|
+
word-wrap: normal;
|
|
255
|
+
}}
|
|
256
|
+
</style>
|
|
257
|
+
</head>
|
|
258
|
+
<body>
|
|
259
|
+
<div class="header">{file_path.name}</div>
|
|
260
|
+
<div class="content">
|
|
261
|
+
<pre>{escaped_content}</pre>
|
|
262
|
+
</div>
|
|
263
|
+
</body>
|
|
264
|
+
</html>"""
|
|
265
|
+
|
|
266
|
+
# Send response
|
|
267
|
+
self.send_response(200)
|
|
268
|
+
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|
269
|
+
self.end_headers()
|
|
270
|
+
self.wfile.write(html_output.encode('utf-8'))
|
|
271
|
+
|
|
272
|
+
except UnicodeDecodeError:
|
|
273
|
+
# If it's not valid UTF-8, serve it normally (might be binary)
|
|
274
|
+
super().do_GET()
|
|
275
|
+
except Exception as e:
|
|
276
|
+
self.send_error(500, f"Error displaying file: {str(e)}")
|
|
277
|
+
|
|
278
|
+
def serve_directory_listing(self):
|
|
279
|
+
try:
|
|
280
|
+
parsed_path = urllib.parse.urlparse(self.path)
|
|
281
|
+
path = parsed_path.path.lstrip('/') or '.'
|
|
282
|
+
path = urllib.parse.unquote(path)
|
|
283
|
+
|
|
284
|
+
dir_path = Path(path)
|
|
285
|
+
if not dir_path.is_dir():
|
|
286
|
+
self.send_error(404, "Directory not found")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
items = sorted(dir_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
|
|
290
|
+
|
|
291
|
+
html = """<!DOCTYPE html>
|
|
292
|
+
<html>
|
|
293
|
+
<head>
|
|
294
|
+
<meta charset="utf-8">
|
|
295
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
296
|
+
<title>Directory listing</title>
|
|
297
|
+
<style>
|
|
298
|
+
body {
|
|
299
|
+
max-width: 800px;
|
|
300
|
+
margin: 40px auto;
|
|
301
|
+
padding: 0 20px;
|
|
302
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
303
|
+
}
|
|
304
|
+
ul { list-style: none; padding: 0; }
|
|
305
|
+
li { margin: 8px 0; }
|
|
306
|
+
a { text-decoration: none; color: #0066cc; }
|
|
307
|
+
a:hover { text-decoration: underline; }
|
|
308
|
+
.dir::before { content: '📁 '; }
|
|
309
|
+
.file::before { content: '📄 '; }
|
|
310
|
+
</style>
|
|
311
|
+
</head>
|
|
312
|
+
<body>
|
|
313
|
+
"""
|
|
314
|
+
html += f"<h1>Directory: /{path}</h1>\n<ul>\n"
|
|
315
|
+
|
|
316
|
+
if path != '.':
|
|
317
|
+
parent = dir_path.parent
|
|
318
|
+
html += f'<li><a href="/{parent}" class="dir">..</a></li>\n'
|
|
319
|
+
|
|
320
|
+
for item in items:
|
|
321
|
+
rel_path = item.relative_to('.')
|
|
322
|
+
if item.is_dir():
|
|
323
|
+
html += f'<li><a href="/{rel_path}" class="dir">{item.name}/</a></li>\n'
|
|
324
|
+
else:
|
|
325
|
+
html += f'<li><a href="/{rel_path}" class="file">{item.name}</a></li>\n'
|
|
326
|
+
|
|
327
|
+
html += "</ul>\n</body>\n</html>"
|
|
328
|
+
|
|
329
|
+
self.send_response(200)
|
|
330
|
+
self.send_header('Content-type', 'text/html; charset=utf-8')
|
|
331
|
+
self.end_headers()
|
|
332
|
+
self.wfile.write(html.encode('utf-8'))
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
self.send_error(500, f"Error listing directory: {str(e)}")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def main():
|
|
339
|
+
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
|
|
340
|
+
|
|
341
|
+
server = HTTPServer(('', port), MarkdownHandler)
|
|
342
|
+
print(f"=" * 60)
|
|
343
|
+
print(f"Markdown Server v{VERSION}")
|
|
344
|
+
print(f"=" * 60)
|
|
345
|
+
print(f"Server: http://localhost:{port}")
|
|
346
|
+
print(f"Features:")
|
|
347
|
+
print(f" - Markdown rendering (.md, .markdown)")
|
|
348
|
+
print(f" - Text file viewer (.py, .js, .gitignore, etc.)")
|
|
349
|
+
print(f" - Directory browsing")
|
|
350
|
+
print(f"=" * 60)
|
|
351
|
+
print("Press Ctrl+C to stop\n")
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
server.serve_forever()
|
|
355
|
+
except KeyboardInterrupt:
|
|
356
|
+
print("\nShutting down...")
|
|
357
|
+
server.shutdown()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if __name__ == '__main__':
|
|
361
|
+
main()
|