mirror of
https://github.com/keathmilligan/obsidian-paste-reformatter.git
synced 2026-07-22 05:43:44 +00:00
Compare commits
13 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02e538218d | ||
|
|
b20b44f218 | ||
|
|
a05bdc269e | ||
|
|
6be4d6ec56 | ||
|
|
4f539dc990 | ||
|
|
c8f5c24e92 | ||
|
|
62a1893a97 | ||
|
|
7435a160b7 | ||
|
|
b80d494bed | ||
|
|
ec8685eddc | ||
|
|
7c4cc6dec9 | ||
|
|
73506e74b6 | ||
|
|
fffeead4a8 |
16 changed files with 3353 additions and 993 deletions
28
.github/workflows/ci.yml
vendored
Normal file
28
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags-ignore:
|
||||
- "*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build plugin
|
||||
run: npm run build
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -32,4 +32,4 @@ jobs:
|
|||
gh release create "$tag" \
|
||||
--title="$tag" \
|
||||
--draft \
|
||||
dist/main.js manifest.json styles.css
|
||||
main.js manifest.json styles.css
|
||||
|
|
|
|||
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -21,11 +21,6 @@ dist/
|
|||
dist
|
||||
build/
|
||||
|
||||
# Dependency lock files (keep package.json)
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
|
@ -33,3 +28,9 @@ coverage/
|
|||
# Obsidian
|
||||
.obsidian/
|
||||
.trash/
|
||||
|
||||
# Gen AI tools
|
||||
.roo/
|
||||
.opencode/
|
||||
.claude/
|
||||
|
||||
|
|
|
|||
73
AGENTS.md
Normal file
73
AGENTS.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Obsidian Plugin Development Guidelines
|
||||
|
||||
This is an Obsidian plugin project. Follow these guidelines when working with the codebase.
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
- Official guide: https://docs.obsidian.md/Plugins/Getting+started/Build+a+plugin
|
||||
- API reference: https://docs.obsidian.md/Reference/TypeScript+API/Plugin
|
||||
- TypeScript API: Use `obsidian` module types
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `main.ts` - Main plugin entry point (extends `Plugin` class)
|
||||
- `manifest.json` - Plugin metadata (id, name, version, minAppVersion)
|
||||
- `styles.css` - Plugin styles (optional)
|
||||
- `src/` - Additional source files and modules
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Security & DOM Manipulation
|
||||
- **Never** use `innerHTML` or `outerHTML` (security risk)
|
||||
- Use DOM APIs (`createElement`, `appendChild`, etc.) or Obsidian helper functions
|
||||
- Sanitize user input before rendering
|
||||
- Prefer `createEl()` and `createDiv()` methods from Obsidian API
|
||||
|
||||
### Styling
|
||||
- Avoid assigning styles via JavaScript or inline HTML
|
||||
- Move all styles to CSS files for better theme compatibility
|
||||
- Use CSS variables for colors and spacing when possible
|
||||
- Follow Obsidian's design language
|
||||
- Use `app.workspace.containerEl.win` to access the window object for styles
|
||||
|
||||
### Settings
|
||||
- Use the `Setting` API for all settings components
|
||||
- Include proper headings and dividers using the Settings API
|
||||
- Use sentence case for all UI text and headings
|
||||
- Organize settings logically into sections
|
||||
- Store settings in a dedicated settings object
|
||||
- Call `saveData()` after settings changes
|
||||
|
||||
### Code Organization
|
||||
- Keep main plugin file lean, delegate to separate modules
|
||||
- Use TypeScript for better type safety
|
||||
- Follow async/await patterns for asynchronous operations
|
||||
- Clean up resources in `onunload()` method
|
||||
- Use `addCommand()` for command palette integration
|
||||
- Use `registerEvent()` for event listeners to ensure cleanup
|
||||
|
||||
### Plugin Lifecycle
|
||||
- `onload()` - Initialize plugin, register events, commands, settings
|
||||
- `onunload()` - Clean up resources, remove event listeners
|
||||
- Use `this.register()` to register cleanup callbacks
|
||||
- Use `this.registerEvent()` for automatic event cleanup
|
||||
|
||||
### Common Patterns
|
||||
- Access app: `this.app`
|
||||
- Access vault: `this.app.vault`
|
||||
- Access workspace: `this.app.workspace`
|
||||
- Read files: `this.app.vault.read(file)`
|
||||
- Modify files: `this.app.vault.modify(file, content)`
|
||||
- Create notices: `new Notice("message")`
|
||||
|
||||
### Development Workflow
|
||||
- Use `npm run dev` to watch and rebuild on changes
|
||||
- Hot reload: Copy built `main.js` to vault's `.obsidian/plugins/` folder
|
||||
- Test in actual Obsidian vault for best results
|
||||
- Check console for errors: View > Toggle Developer Tools
|
||||
|
||||
### Performance
|
||||
- Avoid blocking the main thread
|
||||
- Use debouncing for frequent operations
|
||||
- Be mindful of large vault operations
|
||||
- Cache expensive computations when appropriate
|
||||
|
|
@ -36,7 +36,7 @@ The Paste Reformatter plugin automatically processes content when you paste it i
|
|||
4. Applies Markdown transformations (heading adjustments, line break handling, regex string replacement)
|
||||
5. Inserts the transformed content at the cursor position
|
||||
|
||||
A notification will appear briefly indicating whether HTML or plain text content was reformatted.
|
||||
A notification will appear briefly indicating whether HTML or plain text content was reformatted. This can be disabled in settings.
|
||||
|
||||
### Commands
|
||||
|
||||
|
|
@ -62,6 +62,10 @@ Paste Reformatter can potentially conflict with other Obsidian plugins that over
|
|||
When this setting is enabled, the default behavior of Obsidian's Paste function will be enhanced. Otherwise,
|
||||
**Reformat and Paste** command can be bound to an alternative hot-key to get enhanced paste behavior.
|
||||
|
||||
#### Show paste notifications
|
||||
|
||||
When enabled, a notice appears after reformatting content. Disable this to hide notifications.
|
||||
|
||||
### HTML Transformations
|
||||
|
||||
These settings control how HTML content is processed before being converted to Markdown.
|
||||
|
|
|
|||
158
cascade-rules.md
158
cascade-rules.md
|
|
@ -1,158 +0,0 @@
|
|||
# Heading Cascade Rules
|
||||
|
||||
## Scanario: Cascade Heading Levels is Disabled
|
||||
|
||||
When Cascade Heading Levels is not enabled, then the Max Heading Level setting simply caps the maximum heading value that is allowed. For instance, if it is set to H3, then H1 and H2 would simply be changed to H3. Otherwise, heading values are not affected.
|
||||
|
||||
### Example
|
||||
|
||||
If Max heading Level is set to H3, then the following text when pasted:
|
||||
|
||||
```
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
```
|
||||
|
||||
Will be transformed into:
|
||||
|
||||
```
|
||||
### Heading 1
|
||||
### Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
|
||||
### Heading 1
|
||||
### Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
```
|
||||
|
||||
## Scenario: Cascade Heading Levels is Enabled
|
||||
|
||||
When Cascade Heading Levels is enabled, the heading values that are greater (in this case "greater" means H1 is greater than H2, H2 is greater than H3, etc.) than the max heading level setting are changed to the max heading level.
|
||||
In addition subsequent headings values are demoted ("cascaded") to the heading level below.
|
||||
|
||||
### Example
|
||||
|
||||
When max heading level is set to H3 and the following text was pasted:
|
||||
|
||||
```
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
```
|
||||
|
||||
The result would be:
|
||||
|
||||
```
|
||||
### Heading 1
|
||||
#### Heading 2
|
||||
##### Heading 3
|
||||
###### Heading 4
|
||||
###### Heading 5
|
||||
###### Heading 6
|
||||
|
||||
### Heading 1
|
||||
#### Heading 2
|
||||
##### Heading 3
|
||||
###### Heading 4
|
||||
###### Heading 5
|
||||
###### Heading 6
|
||||
```
|
||||
|
||||
(heading levels will never be demoted below H6, e.g. "######")
|
||||
|
||||
### Another Example
|
||||
|
||||
When Max Heading Level is set to H2. The following should remain unchanged:
|
||||
|
||||
```
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
```
|
||||
|
||||
# Contextual Cascade Rules
|
||||
|
||||
Contextual Cascade is completely independent of Cascade Heading Levels.
|
||||
|
||||
## Scenario: Contextual Cascade is Disabled
|
||||
|
||||
When Contextual Cascade is disabled, then the normal Max Heading Level and Cascade Heading Levels rules apply as usual.
|
||||
|
||||
## Scenario: Contextual Cascade is Enabled
|
||||
|
||||
When Contextual Cascade is enabled, then the Max Heading Level and Cascade Heading Levels rules are superceded by the Contextual Cascade rules:
|
||||
|
||||
### Example
|
||||
|
||||
When text is pasted into an H2 section, for example, then headings are cascaded down from H3. For example, if the following text was pasted into an H2 section:
|
||||
|
||||
```
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
### Heading 3
|
||||
#### Heading 4
|
||||
##### Heading 5
|
||||
###### Heading 6
|
||||
```
|
||||
|
||||
The result would be:
|
||||
|
||||
```
|
||||
### Heading 1
|
||||
#### Heading 2
|
||||
##### Heading 3
|
||||
###### Heading 4
|
||||
###### Heading 5
|
||||
###### Heading 6
|
||||
|
||||
### Heading 1
|
||||
#### Heading 2
|
||||
##### Heading 3
|
||||
###### Heading 4
|
||||
###### Heading 5
|
||||
###### Heading 6
|
||||
```
|
||||
|
||||
As with regular cascading, the heading levels will never be demoted below H6, e.g. "######".
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
// copy-styles.mjs
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Ensure the dist directory exists
|
||||
async function ensureDir(dir) {
|
||||
try {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
} catch (error) {
|
||||
if (error.code !== 'EEXIST') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyFiles() {
|
||||
const distDir = path.resolve('./dist');
|
||||
await ensureDir(distDir);
|
||||
|
||||
try {
|
||||
// Copy styles.css to dist directory
|
||||
await fs.copyFile('./styles.css', path.join(distDir, 'styles.css'));
|
||||
console.log('Successfully copied styles.css to dist directory');
|
||||
|
||||
// Copy manifest.json to dist directory
|
||||
await fs.copyFile('./manifest.json', path.join(distDir, 'manifest.json'));
|
||||
console.log('Successfully copied manifest.json to dist directory');
|
||||
} catch (error) {
|
||||
console.error('Error copying files:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
copyFiles();
|
||||
|
|
@ -3,7 +3,7 @@ import process from "process";
|
|||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
|
|
@ -12,38 +12,38 @@ if you want to view the source, please visit the github repository of this plugi
|
|||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "dist/main.js",
|
||||
minify: prod,
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
minify: prod,
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
await context.watch();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "paste-reformatter",
|
||||
"name": "Paste Reformatter",
|
||||
"version": "1.2.1",
|
||||
"version": "1.4.2",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Reformat pasted text for precise control.",
|
||||
"author": "Keath Milligan",
|
||||
|
|
|
|||
2330
package-lock.json
generated
Normal file
2330
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
44
package.json
44
package.json
|
|
@ -1,24 +1,24 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.2.1",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs && node copy-styles.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production && node copy-styles.mjs",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
"name": "obsidian-paste-reformatter",
|
||||
"version": "1.4.2",
|
||||
"description": "Reformat pasted text for precise control.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
export function transformHTML(
|
||||
html: string,
|
||||
settings: {
|
||||
htmlRegexReplacements: Array<{pattern: string, replacement: string}>,
|
||||
htmlRegexReplacements: Array<{ pattern: string, replacement: string }>,
|
||||
stripLineBreaks: boolean,
|
||||
removeEmptyElements: boolean
|
||||
}
|
||||
|
|
@ -32,11 +32,11 @@ export function transformHTML(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create a temporary DOM element to parse the HTML
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
|
||||
// Process line breaks if strip line breaks is enabled
|
||||
if (settings.stripLineBreaks) {
|
||||
// Find all <br> elements and remove them
|
||||
|
|
@ -45,26 +45,8 @@ export function transformHTML(
|
|||
br.remove();
|
||||
});
|
||||
appliedTransformations = true;
|
||||
} else {
|
||||
// If we're not stripping line breaks, convert them to special paragraph tags
|
||||
// that will be preserved even if empty elements are removed
|
||||
const brElements = doc.querySelectorAll('br');
|
||||
brElements.forEach(br => {
|
||||
// Create a special paragraph with a class that marks it as a line break
|
||||
const lineBreakP = doc.createElement('p');
|
||||
lineBreakP.className = 'preserve-line-break';
|
||||
lineBreakP.setAttribute('data-preserve', 'true');
|
||||
|
||||
// Add a non-breaking space as placeholder content
|
||||
// This ensures the paragraph isn't considered empty when Remove Empty Lines is enabled
|
||||
// The unicode character will be invisible but ensures the line isn't empty
|
||||
lineBreakP.textContent = '\u200B'; // Zero-width space
|
||||
|
||||
// Replace the <br> with our special paragraph
|
||||
br.parentNode?.replaceChild(lineBreakP, br);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Remove empty elements if enabled
|
||||
if (settings.removeEmptyElements) {
|
||||
// Function to check if an element is empty (no text content and no meaningful children)
|
||||
|
|
@ -73,35 +55,35 @@ export function transformHTML(
|
|||
if (['img', 'hr', 'br', 'input', 'iframe'].includes(element.tagName.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Skip our special line break paragraphs
|
||||
if (element.hasAttribute('data-preserve')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check if it has any text content (include whitespace)
|
||||
if (element.textContent && element.textContent.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check if it has any non-empty children
|
||||
for (let i = 0; i < element.children.length; i++) {
|
||||
if (!isElementEmpty(element.children[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
// Find and remove empty elements
|
||||
// We need to use a while loop because the DOM changes as we remove elements
|
||||
let emptyElementsFound = true;
|
||||
while (emptyElementsFound) {
|
||||
emptyElementsFound = false;
|
||||
|
||||
|
||||
// Target common empty elements
|
||||
const potentialEmptyElements = doc.querySelectorAll('p:not([data-preserve]), div, span, li, ul, ol, table, tr, td, th');
|
||||
const potentialEmptyElements = doc.querySelectorAll('p, div, span, li, ul, ol, table, tr, td, th');
|
||||
potentialEmptyElements.forEach(element => {
|
||||
if (isElementEmpty(element)) {
|
||||
element.remove();
|
||||
|
|
@ -109,14 +91,14 @@ export function transformHTML(
|
|||
appliedTransformations = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// If no more empty elements are found, exit the loop
|
||||
if (!emptyElementsFound) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Return the modified HTML and transformation status
|
||||
const serializer = new XMLSerializer();
|
||||
return {
|
||||
|
|
|
|||
1381
src/main.ts
1381
src/main.ts
File diff suppressed because it is too large
Load diff
|
|
@ -11,11 +11,12 @@
|
|||
export function transformMarkdown(
|
||||
markdown: string,
|
||||
settings: {
|
||||
markdownRegexReplacements: Array<{pattern: string, replacement: string}>,
|
||||
markdownRegexReplacements: Array<{ pattern: string, replacement: string }>,
|
||||
contextualCascade: boolean,
|
||||
maxHeadingLevel: number,
|
||||
cascadeHeadingLevels: boolean,
|
||||
stripLineBreaks: boolean,
|
||||
convertToSingleSpaced: boolean,
|
||||
removeEmptyLines: boolean
|
||||
},
|
||||
contextLevel: number = 0,
|
||||
|
|
@ -23,26 +24,38 @@ export function transformMarkdown(
|
|||
): { markdown: string, appliedTransformations: boolean } {
|
||||
let appliedTransformations = false;
|
||||
|
||||
console.log(`original: ${markdown}`);
|
||||
|
||||
// Apply regex replacements if defined
|
||||
if (settings.markdownRegexReplacements && settings.markdownRegexReplacements.length > 0) {
|
||||
for (const replacement of settings.markdownRegexReplacements) {
|
||||
for (const regex_replacement of settings.markdownRegexReplacements) {
|
||||
try {
|
||||
const regex = new RegExp(replacement.pattern, 'g');
|
||||
const regex = new RegExp(regex_replacement.pattern, 'g');
|
||||
const replacement = regex_replacement.replacement
|
||||
.replace(/\\r\\n/g, '\r\n')
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\r/g, '\r')
|
||||
.replace(/\\t/g, '\t')
|
||||
.replace(/\\'/g, "'")
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\\\/g, '\\');
|
||||
const originalMarkdown = markdown;
|
||||
markdown = markdown.replace(regex, replacement.replacement);
|
||||
// console.log(`applying ${JSON.stringify(regex_replacement.pattern)} replacement ${JSON.stringify(replacement)}`); console.log(JSON.stringify(markdown));
|
||||
markdown = markdown.replace(regex, replacement);
|
||||
if (originalMarkdown !== markdown) {
|
||||
appliedTransformations = true;
|
||||
console.log(`regex replacements: ${markdown}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error applying markdown regex replacement: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!escapeMarkdown) {
|
||||
// Find all heading lines
|
||||
const headingRegex = /^(#{1,6})\s/gm;
|
||||
|
||||
|
||||
// Process headings based on settings
|
||||
if (settings.contextualCascade && contextLevel > 0) {
|
||||
let delta = -1;
|
||||
|
|
@ -62,12 +75,12 @@ export function transformMarkdown(
|
|||
newLevel = Math.min(contextLevel + 1, 6);
|
||||
delta = newLevel - currentLevel;
|
||||
cascading = true;
|
||||
console.log(`contextual cascade initiated: delta: ${delta}`);
|
||||
console.log(`*** contextual cascade initiated: delta: ${delta}`);
|
||||
} // else nothing to do
|
||||
|
||||
console.log(`result: current level: ${currentLevel}, new level: ${newLevel}`);
|
||||
|
||||
appliedTransformations = (newLevel !== currentLevel);
|
||||
|
||||
appliedTransformations = appliedTransformations || (newLevel !== currentLevel);
|
||||
// Return the new heading with the adjusted level
|
||||
return '#'.repeat(newLevel) + ' ';
|
||||
});
|
||||
|
|
@ -78,7 +91,7 @@ export function transformMarkdown(
|
|||
markdown = markdown.replace(headingRegex, (match, hashes) => {
|
||||
const currentLevel = hashes.length;
|
||||
let newLevel = currentLevel;
|
||||
|
||||
|
||||
if (settings.cascadeHeadingLevels) {
|
||||
// If cascading is enabled, start cascading subsequent headings down if needed
|
||||
if (cascading) {
|
||||
|
|
@ -95,10 +108,10 @@ export function transformMarkdown(
|
|||
// Cascading not enabled, just cap heading levels at max
|
||||
newLevel = Math.max(currentLevel, settings.maxHeadingLevel)
|
||||
}
|
||||
|
||||
|
||||
console.log(`result: current level: ${currentLevel}, new level: ${newLevel}`);
|
||||
|
||||
appliedTransformations = (newLevel !== currentLevel);
|
||||
appliedTransformations = appliedTransformations || (newLevel !== currentLevel);
|
||||
|
||||
// Return the new heading with the adjusted level
|
||||
return '#'.repeat(newLevel) + ' ';
|
||||
|
|
@ -108,7 +121,7 @@ export function transformMarkdown(
|
|||
// If escaping markdown, we don't want to change headings
|
||||
// Just escape the markdown content
|
||||
const originalMarkdown = markdown;
|
||||
|
||||
|
||||
// Escape all Markdown syntax that Obsidian recognizes
|
||||
// Headings, bold/italic, lists, links, images, code blocks, blockquotes, etc.
|
||||
markdown = markdown
|
||||
|
|
@ -139,60 +152,83 @@ export function transformMarkdown(
|
|||
// Otherwise it's an HTML tag that needs escaping
|
||||
return htmlTag ? '`' + htmlTag + '`' : match;
|
||||
});
|
||||
|
||||
|
||||
appliedTransformations = (originalMarkdown !== markdown);
|
||||
}
|
||||
|
||||
console.log(`processed: ${markdown}`);
|
||||
|
||||
// First handle the special line break markers
|
||||
let preserveLineBreaks = !settings.stripLineBreaks;
|
||||
|
||||
// If we're not stripping line breaks, we need to handle the special markers
|
||||
if (preserveLineBreaks) {
|
||||
// Replace special line break markers with a unique placeholder that won't be affected by empty line removal
|
||||
const lineBreakPlaceholder = '___LINE_BREAK_PLACEHOLDER___';
|
||||
markdown = markdown.replace(/<p class="preserve-line-break"[^>]*>.*?<\/p>/g, lineBreakPlaceholder);
|
||||
markdown = markdown.replace(/<p data-preserve="true"[^>]*>.*?<\/p>/g, lineBreakPlaceholder);
|
||||
|
||||
// Remove empty lines if enabled
|
||||
if (settings.removeEmptyLines) {
|
||||
// First, normalize line endings to ensure consistent processing
|
||||
markdown = markdown.replace(/\r\n/g, '\n');
|
||||
|
||||
// Split the content into lines
|
||||
const lines = markdown.split('\n');
|
||||
|
||||
// Filter out empty lines, but keep our placeholders
|
||||
const filteredLines = lines.filter(line => {
|
||||
return line.trim() !== '' || line.includes(lineBreakPlaceholder);
|
||||
});
|
||||
|
||||
// Join the filtered lines back together
|
||||
const originalMarkdown = markdown;
|
||||
markdown = filteredLines.join('\n');
|
||||
appliedTransformations = (originalMarkdown !== markdown);
|
||||
}
|
||||
|
||||
// Now replace our placeholders with actual line breaks
|
||||
markdown = markdown.replace(new RegExp(lineBreakPlaceholder, 'g'), '\n');
|
||||
} else {
|
||||
// If we're stripping line breaks, just remove empty lines normally
|
||||
if (settings.removeEmptyLines) {
|
||||
// First, normalize line endings to ensure consistent processing
|
||||
markdown = markdown.replace(/\r\n/g, '\n');
|
||||
|
||||
// Split the content into lines
|
||||
const lines = markdown.split('\n');
|
||||
|
||||
// Filter out all empty lines
|
||||
const filteredLines = lines.filter(line => line.trim() !== '');
|
||||
|
||||
// Join the filtered lines back together
|
||||
const originalMarkdown = markdown;
|
||||
markdown = filteredLines.join('\n');
|
||||
appliedTransformations = (originalMarkdown !== markdown);
|
||||
// Convert multiple consecutive blank lines to single blank line if enabled
|
||||
// Skip this if removeEmptyLines is enabled (optimization - lines will be removed anyway)
|
||||
if (settings.convertToSingleSpaced && !settings.removeEmptyLines) {
|
||||
// Normalize line endings to ensure consistent processing
|
||||
markdown = markdown.replace(/\r\n/g, '\n');
|
||||
|
||||
// Replace 2 or more consecutive newlines with exactly 2 newlines (1 blank line)
|
||||
const originalMarkdown = markdown;
|
||||
markdown = markdown.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
if (originalMarkdown !== markdown) {
|
||||
appliedTransformations = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Process empty lines with a sliding window approach
|
||||
if (settings.removeEmptyLines) {
|
||||
// First, normalize line endings to ensure consistent processing
|
||||
markdown = markdown.replace(/\r\n/g, '\n');
|
||||
|
||||
// Split the content into lines
|
||||
const lines = markdown.split('\n');
|
||||
const filteredLines: string[] = [];
|
||||
|
||||
// Sliding window processing with peek capability
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const currentLine = lines[i];
|
||||
const nextLine = i + 1 < lines.length ? lines[i + 1] : null;
|
||||
const isCurrentLineEmpty = currentLine.trim() === '';
|
||||
|
||||
// Rule 1: Preserve line breaks - check for special markers
|
||||
if (preserveLineBreaks) {
|
||||
const hasPreserveMarker =
|
||||
/<p class="preserve-line-break"[^>]*>.*?<\/p>/.test(currentLine) ||
|
||||
/<p data-preserve="true"[^>]*>.*?<\/p>/.test(currentLine);
|
||||
|
||||
if (hasPreserveMarker) {
|
||||
// Insert an empty line instead of the marker
|
||||
filteredLines.push('');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 2: Keep empty line if next line is a horizontal rule (3+ dashes)
|
||||
if (isCurrentLineEmpty && nextLine !== null && /^\s*-{3,}\s*$/.test(nextLine)) {
|
||||
filteredLines.push(currentLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rule 3: Keep empty line if next line is the beginning of a table
|
||||
if (isCurrentLineEmpty && nextLine !== null && /^\s*\|.*\|/.test(nextLine)) {
|
||||
filteredLines.push(currentLine);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Default: Remove empty lines unless they meet the above criteria
|
||||
if (!isCurrentLineEmpty) {
|
||||
filteredLines.push(currentLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Join the filtered lines back together
|
||||
const originalMarkdown = markdown;
|
||||
markdown = filteredLines.join('\n');
|
||||
appliedTransformations = appliedTransformations || (originalMarkdown !== markdown);
|
||||
}
|
||||
|
||||
console.log(`final: ${markdown}`);
|
||||
return {
|
||||
markdown,
|
||||
appliedTransformations
|
||||
|
|
|
|||
11
styles.css
11
styles.css
|
|
@ -115,3 +115,14 @@ If your plugin does not need CSS, delete this file.
|
|||
outline: 2px solid var(--interactive-accent);
|
||||
outline-offset: 2px;
|
||||
} */
|
||||
|
||||
/* Disabled Setting Styles */
|
||||
.paste-reformatter-disabled-setting {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.paste-reformatter-disabled-setting .setting-item-name,
|
||||
.paste-reformatter-disabled-setting .setting-item-description {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,9 @@
|
|||
"1.1.0": "0.15.0",
|
||||
"1.1.1": "0.15.0",
|
||||
"1.2.0": "0.15.0",
|
||||
"1.2.1": "0.15.0"
|
||||
"1.2.1": "0.15.0",
|
||||
"1.2.2": "0.15.0",
|
||||
"1.3.0": "0.15.0",
|
||||
"1.4.0": "0.15.0",
|
||||
"1.4.2": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue