Merge pull request #146 from gamosoft/features/plain_password
Features/plain password
This commit is contained in:
commit
2fd403b04a
27
README.md
27
README.md
|
|
@ -146,6 +146,33 @@ python run.py
|
|||
- Python 3.8 or higher
|
||||
- pip (Python package manager)
|
||||
|
||||
#### Using Virtual Environments (Recommended for Arch/Fedora/Ubuntu 23.04+)
|
||||
|
||||
Modern Linux distributions enforce [PEP 668](https://peps.python.org/pep-0668/), which prevents system-wide pip installs. Use a virtual environment instead:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/gamosoft/notediscovery.git
|
||||
cd notediscovery
|
||||
|
||||
# Create a virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# Activate it (choose your shell):
|
||||
source venv/bin/activate # Bash/Zsh (most Linux distros)
|
||||
source venv/bin/activate.fish # Fish (CachyOS, etc.)
|
||||
source venv/bin/activate.csh # Csh/Tcsh
|
||||
.\venv\Scripts\activate # Windows PowerShell
|
||||
|
||||
# Install dependencies and run
|
||||
pip install -r requirements.txt
|
||||
python run.py
|
||||
```
|
||||
|
||||
> ⚠️ **Warning**
|
||||
>
|
||||
> *You'll need to activate the virtual environment (source venv/bin/activate) each time you open a new terminal before running the app*
|
||||
|
||||
### Advanced Docker Setup
|
||||
|
||||
The image includes bundled config, themes, plugins, and locales. To customize, you must:
|
||||
|
|
|
|||
|
|
@ -67,10 +67,34 @@ if 'AUTHENTICATION_ENABLED' in os.environ:
|
|||
else:
|
||||
print(f"🔐 Authentication {'ENABLED' if config.get('authentication', {}).get('enabled', False) else 'DISABLED'} (from config.yaml)")
|
||||
|
||||
# Allow password hash to be set via environment variable (useful for demos)
|
||||
if 'AUTHENTICATION_PASSWORD_HASH' in os.environ:
|
||||
# Password configuration priority:
|
||||
# 1. AUTHENTICATION_PASSWORD env var (plain text, hashed at startup)
|
||||
# 2. AUTHENTICATION_PASSWORD_HASH env var (pre-hashed)
|
||||
# 3. authentication.password in config.yaml (plain text, hashed at startup)
|
||||
# 4. authentication.password_hash in config.yaml (pre-hashed)
|
||||
# Default password is "admin" if nothing is configured
|
||||
if 'AUTHENTICATION_PASSWORD' in os.environ:
|
||||
# Plain text password from env var - hash it
|
||||
plain_password = os.getenv('AUTHENTICATION_PASSWORD')
|
||||
config['authentication']['password_hash'] = bcrypt.hashpw(
|
||||
plain_password.encode('utf-8'),
|
||||
bcrypt.gensalt()
|
||||
).decode('utf-8')
|
||||
print("🔑 Password loaded from AUTHENTICATION_PASSWORD env var (hashed at startup)")
|
||||
elif 'AUTHENTICATION_PASSWORD_HASH' in os.environ:
|
||||
# Pre-hashed password from env var
|
||||
config['authentication']['password_hash'] = os.getenv('AUTHENTICATION_PASSWORD_HASH')
|
||||
print("🔑 Password hash loaded from AUTHENTICATION_PASSWORD_HASH env var")
|
||||
elif config.get('authentication', {}).get('password'):
|
||||
# Plain text password from config.yaml - hash it
|
||||
plain_password = config['authentication']['password']
|
||||
config['authentication']['password_hash'] = bcrypt.hashpw(
|
||||
plain_password.encode('utf-8'),
|
||||
bcrypt.gensalt()
|
||||
).decode('utf-8')
|
||||
# Clear the plain text password from config for security
|
||||
del config['authentication']['password']
|
||||
print("🔑 Password loaded from config.yaml (hashed at startup)")
|
||||
|
||||
# Allow secret key to be set via environment variable (for session security)
|
||||
if 'AUTHENTICATION_SECRET_KEY' in os.environ:
|
||||
|
|
|
|||
11
config.yaml
11
config.yaml
|
|
@ -35,9 +35,16 @@ authentication:
|
|||
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
|
||||
secret_key: "change_this_to_a_random_secret_key_in_production"
|
||||
|
||||
# Password hash - Generate with: python generate_password.py
|
||||
# Password configuration - choose ONE of the following options:
|
||||
#
|
||||
# Option 1: Plain text password (recommended for ease of use)
|
||||
# The password is hashed automatically at startup
|
||||
# ⚠️ Default password is "admin" - CHANGE THIS for production!
|
||||
password_hash: "$2b$12$t/6PGExFzdpU2PUta0iVY.eDQwvu63kH.c/d4bEnnHaQ5CspH1yrG" # Default: "admin"
|
||||
password: "admin"
|
||||
|
||||
# Option 2: Pre-hashed password (for advanced users)
|
||||
# Generate with: python generate_password.py
|
||||
# password_hash: "$2b$12$..."
|
||||
|
||||
# Session expiry in seconds (default: 7 days)
|
||||
session_max_age: 604800
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@
|
|||
animation: fadeInScale 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Stagger animation delays for feature cards */
|
||||
/* Stagger animation delays for feature cards (5 rows × 3 cols = 15 cards) */
|
||||
.feature.animated:nth-child(1) { animation-delay: 0s; }
|
||||
.feature.animated:nth-child(2) { animation-delay: 0.1s; }
|
||||
.feature.animated:nth-child(3) { animation-delay: 0.2s; }
|
||||
|
|
@ -367,6 +367,8 @@
|
|||
.feature.animated:nth-child(11) { animation-delay: 0.25s; }
|
||||
.feature.animated:nth-child(12) { animation-delay: 0.35s; }
|
||||
.feature.animated:nth-child(13) { animation-delay: 0.2s; }
|
||||
.feature.animated:nth-child(14) { animation-delay: 0.3s; }
|
||||
.feature.animated:nth-child(15) { animation-delay: 0.4s; }
|
||||
|
||||
.screenshot-section {
|
||||
margin: 4rem 0 3rem 0;
|
||||
|
|
@ -832,12 +834,6 @@
|
|||
<p>No subscriptions, no hidden fees. Free and open source forever.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature animate-on-scroll">
|
||||
<div class="feature-icon">🚀</div>
|
||||
<h3>Lightning Fast</h3>
|
||||
<p>Instant search and navigation. Works offline, always responsive.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature animate-on-scroll">
|
||||
<div class="feature-icon">🧮</div>
|
||||
<h3>LaTeX Math</h3>
|
||||
|
|
@ -868,12 +864,6 @@
|
|||
<p>Create from custom templates with dynamic placeholders. Meeting notes, journals, and more.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature animate-on-scroll">
|
||||
<div class="feature-icon">🔗</div>
|
||||
<h3>Wikilinks</h3>
|
||||
<p>Link notes with [[double brackets]] Obsidian-style. Jump to sections with [[note#heading]] anchors.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature animate-on-scroll">
|
||||
<div class="feature-icon">🕸️</div>
|
||||
<h3>Graph View</h3>
|
||||
|
|
@ -909,6 +899,18 @@
|
|||
<h3>Extensible</h3>
|
||||
<p>Plugin system lets you add custom features and integrations.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature animate-on-scroll">
|
||||
<div class="feature-icon">📲</div>
|
||||
<h3>Install as App</h3>
|
||||
<p>Progressive Web App (PWA) - install on desktop or mobile for a native experience.</p>
|
||||
</div>
|
||||
|
||||
<div class="feature animate-on-scroll">
|
||||
<div class="feature-icon">🌍</div>
|
||||
<h3>Multi-Language</h3>
|
||||
<p>Built-in translations for English, Spanish, German, and French. Easy to add more.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefits animate-on-scroll">
|
||||
|
|
@ -946,38 +948,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<span class="emoji">🌙</span>
|
||||
<div class="text">
|
||||
<strong>Dark Mode</strong>
|
||||
Easy on the eyes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<span class="emoji">💻</span>
|
||||
<div class="text">
|
||||
<strong>Self-Hosted</strong>
|
||||
You own your data
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<span class="emoji">🔗</span>
|
||||
<div class="text">
|
||||
<strong>Wikilinks</strong>
|
||||
[[Link notes]] Obsidian-style
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<span class="emoji">🕸️</span>
|
||||
<div class="text">
|
||||
<strong>Graph View</strong>
|
||||
Visualize note connections
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<span class="emoji">🖼️</span>
|
||||
<div class="text">
|
||||
|
|
@ -1011,10 +981,10 @@
|
|||
</div>
|
||||
|
||||
<div class="benefit-item">
|
||||
<span class="emoji">⚙️</span>
|
||||
<span class="emoji">🔗</span>
|
||||
<div class="text">
|
||||
<strong>Properties Panel</strong>
|
||||
View frontmatter metadata at a glance
|
||||
<strong>Wikilinks</strong>
|
||||
[[Link notes]] Obsidian-style
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,179 +1,165 @@
|
|||
# 🔒 NoteDiscovery Authentication Guide
|
||||
|
||||
## ⚠️ **IMPORTANT: Default Password Warning**
|
||||
## ⚠️ Default Password Warning
|
||||
|
||||
> **The default `config.yaml` includes authentication disabled by default, with password: `admin`**
|
||||
>
|
||||
> 🔴 **CHANGE THIS if you're exposing NoteDiscovery to a network!**
|
||||
>
|
||||
> The default configuration is provided for **quick testing only**. Follow the setup guide below to set your own secure password and secret key.
|
||||
> **Default password is `admin`** — CHANGE THIS before exposing to any network!
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
NoteDiscovery includes a simple, secure authentication system for single-user deployments. When enabled, users must log in with a password before accessing the application.
|
||||
NoteDiscovery includes simple password protection for single-user deployments. When enabled, users must log in before accessing notes.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- ✅ **Single User** - Perfect for personal/self-hosted use
|
||||
- ✅ **Secure** - Passwords hashed with bcrypt
|
||||
- ✅ **Session-based** - Stay logged in for 7 days (configurable)
|
||||
- ✅ Single user / self-hosted use
|
||||
- ✅ Passwords hashed with bcrypt
|
||||
- ✅ Session-based (7 days default, configurable)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Setup
|
||||
## Quick Test (Local Only)
|
||||
|
||||
**Default Configuration:**
|
||||
- Authentication is **disabled by default**
|
||||
- Default password is `admin`
|
||||
- Default secret key is insecure
|
||||
For local testing, authentication is **disabled by default**. To test with auth:
|
||||
|
||||
**⚠️ IMPORTANT:** For production or network-exposed deployments, **change both the password and secret key immediately**.
|
||||
|
||||
---
|
||||
|
||||
### 🧪 **Quick Test (Use Default Password)**
|
||||
|
||||
For **local testing only**, you can use the default configuration:
|
||||
|
||||
1. Start NoteDiscovery (Docker or locally)
|
||||
2. Navigate to `http://localhost:8000`
|
||||
1. Set `authentication.enabled: true` in `config.yaml`
|
||||
2. Restart the app
|
||||
3. Log in with password: `admin`
|
||||
|
||||
**⚠️ Only use this for local testing on your own machine!**
|
||||
⚠️ **Don't use the default password on any network!**
|
||||
|
||||
---
|
||||
|
||||
### 🔒 **Production Setup (Change Password & Secret Key)**
|
||||
## Production Setup
|
||||
|
||||
For any deployment exposed to a network, follow these steps:
|
||||
|
||||
### Step 1: Generate a Password Hash
|
||||
### Step 1: Generate a Secret Key
|
||||
|
||||
Choose your environment:
|
||||
|
||||
**Docker Users:**
|
||||
The secret key encrypts session cookies. Generate a random one:
|
||||
|
||||
```bash
|
||||
# Docker Compose with the GHCR compose file
|
||||
docker-compose -f docker-compose.ghcr.yml exec notediscovery python generate_password.py
|
||||
|
||||
# Or with docker run
|
||||
docker exec -it notediscovery python generate_password.py
|
||||
```
|
||||
|
||||
**Local Users:**
|
||||
|
||||
```bash
|
||||
# Install bcrypt if not already installed
|
||||
pip install bcrypt
|
||||
|
||||
# Run the password generator
|
||||
python generate_password.py
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Prompt you for your password (input is hidden)
|
||||
2. Ask you to confirm it
|
||||
3. Generate a bcrypt hash
|
||||
4. Display the hash with instructions
|
||||
|
||||
**Copy the hash** - you'll need it for Step 3.
|
||||
|
||||
### Step 2: Generate a Secret Key
|
||||
|
||||
Generate a random secret key for session encryption:
|
||||
|
||||
**Docker Users:**
|
||||
```bash
|
||||
docker-compose exec notediscovery python -c "import secrets; print(secrets.token_hex(32))"
|
||||
|
||||
# Or with docker run
|
||||
# Docker
|
||||
docker exec -it notediscovery python -c "import secrets; print(secrets.token_hex(32))"
|
||||
```
|
||||
|
||||
**Local Users:**
|
||||
```bash
|
||||
# Local
|
||||
python -c "import secrets; print(secrets.token_hex(32))"
|
||||
```
|
||||
|
||||
**Copy the key** - you'll need it for Step 3.
|
||||
|
||||
### Step 3: Update `config.yaml`
|
||||
|
||||
Edit your `config.yaml` and update the authentication section:
|
||||
|
||||
```yaml
|
||||
authentication:
|
||||
# Enable authentication
|
||||
enabled: true
|
||||
|
||||
# Session secret key (paste the output from Step 2)
|
||||
secret_key: "your_generated_secret_key_here"
|
||||
|
||||
# Password hash (paste the output from Step 1)
|
||||
password_hash: "$2b$12$..."
|
||||
|
||||
# Session expiry in seconds (7 days by default)
|
||||
session_max_age: 604800
|
||||
```
|
||||
|
||||
### Step 4: Restart the Application
|
||||
|
||||
```bash
|
||||
# If running locally
|
||||
uvicorn backend.main:app --reload
|
||||
|
||||
# If using Docker Compose
|
||||
docker-compose restart
|
||||
|
||||
# Or with docker run
|
||||
docker restart notediscovery
|
||||
```
|
||||
|
||||
### Step 5: Test Login
|
||||
|
||||
Navigate to `http://localhost:8000` and you'll be redirected to the login page.
|
||||
|
||||
Enter the password you chose in Step 2.
|
||||
**Save this key** — you'll need it in Step 2.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Considerations
|
||||
### Step 2: Configure Authentication
|
||||
|
||||
Choose **one** of these options:
|
||||
|
||||
#### Option A: Plain Text Password (Recommended)
|
||||
|
||||
The easiest approach. Your password is automatically hashed at startup.
|
||||
|
||||
**Via Environment Variables (Docker):**
|
||||
```bash
|
||||
docker run -d \
|
||||
-e AUTHENTICATION_ENABLED=true \
|
||||
-e AUTHENTICATION_PASSWORD=your_secure_password \
|
||||
-e AUTHENTICATION_SECRET_KEY=your_generated_secret_key \
|
||||
...
|
||||
```
|
||||
|
||||
**Via config.yaml:**
|
||||
```yaml
|
||||
authentication:
|
||||
enabled: true
|
||||
password: "your_secure_password"
|
||||
secret_key: "your_generated_secret_key"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Option B: Pre-Hashed Password (Advanced)
|
||||
|
||||
For users who prefer to hash passwords themselves.
|
||||
|
||||
**Generate a hash:**
|
||||
```bash
|
||||
# Docker
|
||||
docker exec -it notediscovery python generate_password.py
|
||||
|
||||
# Local
|
||||
python generate_password.py
|
||||
```
|
||||
|
||||
**Then configure:**
|
||||
```yaml
|
||||
authentication:
|
||||
enabled: true
|
||||
password_hash: "$2b$12$..." # paste your hash here
|
||||
secret_key: "your_generated_secret_key"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Restart & Test
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker-compose restart
|
||||
|
||||
# Docker run
|
||||
docker restart notediscovery
|
||||
|
||||
# Local
|
||||
python run.py
|
||||
```
|
||||
|
||||
Navigate to `http://localhost:8000` — you'll be redirected to the login page.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Priority
|
||||
|
||||
If multiple sources are configured, this priority applies (first wins):
|
||||
|
||||
| Priority | Source | Type |
|
||||
|----------|--------|------|
|
||||
| 1st | `AUTHENTICATION_PASSWORD` env var | Plain text |
|
||||
| 2nd | `AUTHENTICATION_PASSWORD_HASH` env var | Pre-hashed |
|
||||
| 3rd | `password` in config.yaml | Plain text |
|
||||
| 4th | `password_hash` in config.yaml | Pre-hashed |
|
||||
|
||||
**Example:** If you set `AUTHENTICATION_PASSWORD` as an env var, it overrides anything in config.yaml.
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### ✅ What This Protects
|
||||
|
||||
- Unauthorized access to your notes
|
||||
- Viewing, creating, editing, and deleting notes
|
||||
- All API endpoints
|
||||
- Viewing, creating, editing, deleting notes
|
||||
|
||||
### ⚠️ What This Doesn't Protect
|
||||
|
||||
This is a **simple authentication system** designed for **self-hosted, single-user** deployments. It is **NOT** suitable for:
|
||||
This is a **simple single-user** system. NOT suitable for:
|
||||
|
||||
- ❌ Multi-user environments
|
||||
- ❌ Public internet exposure without HTTPS
|
||||
- ❌ Production SaaS applications
|
||||
- ❌ Public internet without HTTPS
|
||||
- ❌ Compliance requirements (HIPAA, GDPR, etc.)
|
||||
|
||||
### 🛡️ Best Practices
|
||||
|
||||
1. **Use HTTPS** - Always run behind a reverse proxy (Traefik, nginx, Caddy) with SSL/TLS
|
||||
2. **Strong Password** - Use at least 12 characters with mixed case, numbers, and symbols
|
||||
3. **Unique Secret Key** - Never reuse secret keys across applications
|
||||
4. **Keep Config Secure** - Don't commit `config.yaml` with real credentials to version control
|
||||
5. **VPN/Private Network** - Keep NoteDiscovery on a private network or behind a VPN
|
||||
1. **Use HTTPS** — Run behind a reverse proxy (Traefik, nginx, Caddy)
|
||||
2. **Strong password** — At least 12 characters, mixed case, numbers, symbols
|
||||
3. **Unique secret key** — Never reuse across applications
|
||||
4. **Keep config secure** — Don't commit credentials to version control
|
||||
|
||||
---
|
||||
|
||||
## 🚫 Disabling Authentication
|
||||
|
||||
To disable authentication and allow open access:
|
||||
## Disabling Authentication
|
||||
|
||||
```yaml
|
||||
authentication:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
Restart the application, and authentication will be bypassed.
|
||||
Restart the app to apply.
|
||||
|
|
|
|||
|
|
@ -17,9 +17,22 @@ NoteDiscovery supports environment variables to override configuration settings,
|
|||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `AUTHENTICATION_ENABLED` | boolean | `config.yaml` | Enable/disable authentication |
|
||||
| `AUTHENTICATION_PASSWORD_HASH` | string | `config.yaml` | Bcrypt password hash |
|
||||
| `AUTHENTICATION_PASSWORD` | string | `admin` | Plain text password (hashed automatically at startup) |
|
||||
| `AUTHENTICATION_PASSWORD_HASH` | string | - | Pre-hashed bcrypt password (for advanced users) |
|
||||
| `AUTHENTICATION_SECRET_KEY` | string | `config.yaml` | Session secret key (for session security) |
|
||||
|
||||
> **Password Priority:** `AUTHENTICATION_PASSWORD` takes precedence over `AUTHENTICATION_PASSWORD_HASH`. If both are set, the plain text password is used.
|
||||
|
||||
#### Example: Setting password via environment variable
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker run -e AUTHENTICATION_ENABLED=true -e AUTHENTICATION_PASSWORD=mysecretpassword ...
|
||||
|
||||
# Docker Compose (in .env file or docker-compose.yml)
|
||||
AUTHENTICATION_PASSWORD=mysecretpassword
|
||||
```
|
||||
|
||||
### Demo Mode
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|
|
|
|||
|
|
@ -318,12 +318,32 @@ Full immersive distraction-free writing experience:
|
|||
- **Easy exit** - Press `Esc`, click exit button, or use shortcut again
|
||||
- **State preserved** - Returns to your previous view mode on exit
|
||||
|
||||
## 📱 Progressive Web App (PWA)
|
||||
|
||||
NoteDiscovery can be installed as a standalone app on your device:
|
||||
|
||||
- **Install as app** - Add to home screen on mobile, or install via browser on desktop
|
||||
- **Standalone mode** - Runs without browser chrome for a native app feel
|
||||
|
||||
### How to Install
|
||||
- **Desktop (Chrome/Edge)**: Click the install icon in the address bar, or Menu → "Install NoteDiscovery"
|
||||
- **Android**: Chrome Menu → "Add to Home Screen"
|
||||
- **iOS**: Safari Share → "Add to Home Screen"
|
||||
|
||||
## 🌍 Internationalization
|
||||
|
||||
- **Multiple languages** - English, Spanish, German, French built-in
|
||||
- **Easy to add** - Drop JSON files in `locales/` folder
|
||||
- **Instant switch** - Change language in Settings without reload
|
||||
- **Community translations** - Contributions welcome!
|
||||
|
||||
## 🚀 Performance
|
||||
|
||||
- **Instant loading** - No lag, no loading spinners
|
||||
- **Efficient caching** - Smart local storage
|
||||
- **Minimal resources** - Runs on modest hardware
|
||||
- **No bloat** - Focused on what matters
|
||||
- **Lightweight** - No heavy frameworks
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -800,9 +800,17 @@ function noteApp() {
|
|||
// Links [text](url)
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<span class="md-link">[$1]</span><span class="md-link-url">($2)</span>');
|
||||
|
||||
// Lists
|
||||
html = html.replace(/^(\s*)([-*+])\s/gm, '$1<span class="md-list">$2</span> ');
|
||||
html = html.replace(/^(\s*)(\d+\.)\s/gm, '$1<span class="md-list">$2</span> ');
|
||||
// Lists - use [ \t] instead of \s to avoid matching newlines
|
||||
// Capture rest of line to handle empty bullets properly (issue #142)
|
||||
html = html.replace(/^(\s*)([-*+])[ \t](.*)$/gm, (match, indent, bullet, rest) => {
|
||||
// If rest is empty/whitespace-only, add zero-width space to prevent line collapse
|
||||
const content = rest.trim() === '' ? '\u200B' : rest;
|
||||
return `${indent}<span class="md-list">${bullet}</span> ${content}`;
|
||||
});
|
||||
html = html.replace(/^(\s*)(\d+\.)[ \t](.*)$/gm, (match, indent, bullet, rest) => {
|
||||
const content = rest.trim() === '' ? '\u200B' : rest;
|
||||
return `${indent}<span class="md-list">${bullet}</span> ${content}`;
|
||||
});
|
||||
|
||||
// Blockquotes
|
||||
html = html.replace(/^(>.*)$/gm, '<span class="md-blockquote">$1</span>');
|
||||
|
|
|
|||
Loading…
Reference in New Issue