mirror of
https://github.com/keathmilligan/obsidian-paste-reformatter.git
synced 2026-07-22 05:43:44 +00:00
Fix transform & paste handling issues (#24)
* fix: extra blank lines being added to initial html transform * fix: ignore selected texted when determining contextual heading level * fix: ignore events already handled by another plugin * feat(ci): add CI workflow * chore: add package-lock.json
This commit is contained in:
parent
a05bdc269e
commit
b20b44f218
7 changed files with 2516 additions and 318 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
|
||||
5
.gitignore
vendored
5
.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/
|
||||
|
|
|
|||
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. "######".
|
||||
2330
package-lock.json
generated
Normal file
2330
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
|
|
@ -45,24 +45,6 @@ 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
|
||||
|
|
@ -101,7 +83,7 @@ export function transformHTML(
|
|||
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();
|
||||
|
|
|
|||
62
src/main.ts
62
src/main.ts
|
|
@ -52,24 +52,24 @@ export default class PasteReformatter extends Plugin {
|
|||
|
||||
// Register discrete command for paste reformatting
|
||||
this.addCommand({
|
||||
id: 'reformat-and-paste',
|
||||
name: 'Reformat and Paste',
|
||||
callback: async () => {
|
||||
try {
|
||||
// Get clipboard data
|
||||
const dataTransfer = await this.getClipboardData();
|
||||
if (dataTransfer) {
|
||||
id: 'reformat-and-paste',
|
||||
name: 'Reformat and Paste',
|
||||
callback: async () => {
|
||||
try {
|
||||
// Get clipboard data
|
||||
const dataTransfer = await this.getClipboardData();
|
||||
if (dataTransfer) {
|
||||
// Paste it into the active editor
|
||||
this.doPaste(dataTransfer);
|
||||
} else {
|
||||
new Notice("Clipboard does not contain HTML or plain text content.");
|
||||
new Notice("Clipboard does not contain HTML or plain text content.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error accessing clipboard:", error);
|
||||
new Notice("Error accessing clipboard. Try using regular paste instead.");
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error accessing clipboard:", error);
|
||||
new Notice("Error accessing clipboard. Try using regular paste instead.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register command to paste with all markdown escaped
|
||||
this.addCommand({
|
||||
|
|
@ -163,8 +163,8 @@ export default class PasteReformatter extends Plugin {
|
|||
|
||||
// Transform HTML before converting to Markdown
|
||||
const result = transformHTML(html, this.settings);
|
||||
console.debug(`Original HTML: ${html}`);
|
||||
console.debug(`Transformed HTML: ${result.html}`);
|
||||
// console.log(`Original HTML: ${html}`);
|
||||
console.log(`Transformed HTML: ${result.html}`);
|
||||
|
||||
// Use Obsidian's built-in htmlToMarkdown function
|
||||
originalMarkdown = htmlToMarkdown(result.html);
|
||||
|
|
@ -175,7 +175,7 @@ export default class PasteReformatter extends Plugin {
|
|||
originalMarkdown = clipboardData.getData('text/plain');
|
||||
} else {
|
||||
// No supported format found
|
||||
console.debug("No HTML or plain text content found in clipboard");
|
||||
// console.log("No HTML or plain text content found in clipboard");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -186,6 +186,7 @@ export default class PasteReformatter extends Plugin {
|
|||
}
|
||||
|
||||
// Apply settings to transform the markdown
|
||||
console.log(`original markdown: ${originalMarkdown}`);
|
||||
const markdownResult = transformMarkdown(originalMarkdown, this.settings, contextLevel, escapeMarkdown);
|
||||
appliedMarkdownTransformations = markdownResult.appliedTransformations;
|
||||
|
||||
|
|
@ -233,7 +234,12 @@ export default class PasteReformatter extends Plugin {
|
|||
}
|
||||
|
||||
onPaste(event: ClipboardEvent) {
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.settings.pasteOverride) {
|
||||
|
||||
// Check if there's content in the clipboard
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData) {
|
||||
|
|
@ -243,31 +249,41 @@ export default class PasteReformatter extends Plugin {
|
|||
// Skip reformatting when the cursor is inside a fenced code block
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (activeView?.editor && this.isCursorInCodeBlock(activeView.editor)) {
|
||||
console.debug("Paste Reformatter plugin skipping paste: cursor is inside a code block");
|
||||
console.log("Paste Reformatter plugin skipping paste: cursor is inside a code block");
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the clipboard data
|
||||
if (this.doPaste(clipboardData)) {
|
||||
// Prevent the default paste behavior
|
||||
console.debug("Default paste behavior overridden by Paste Reformatter plugin");
|
||||
console.log("Default paste behavior overridden by Paste Reformatter plugin");
|
||||
event.preventDefault();
|
||||
} else {
|
||||
// If the plugin didn't handle the paste, allow the default behavior
|
||||
console.debug("Paste Reformatter plugin did not handle paste, allowing default behavior");
|
||||
console.log("Paste Reformatter plugin did not handle paste, allowing default behavior");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the current heading level at the cursor position
|
||||
* Determines the current heading level at the cursor position.
|
||||
* If text is selected, the selection is excluded from the search
|
||||
* since it will be replaced by the pasted content.
|
||||
* @param editor The editor instance
|
||||
* @returns The heading level (1-6) or 0 if not in a heading section
|
||||
*/
|
||||
getCurrentHeadingLevel(editor: any): number {
|
||||
// Get the current cursor position
|
||||
const cursor = editor.getCursor();
|
||||
const currentLine = cursor.line;
|
||||
let currentLine = cursor.line;
|
||||
|
||||
// If text is selected, ignore the selected text when looking backward
|
||||
// since it is going to be replaced
|
||||
if (editor.somethingSelected()) {
|
||||
const anchor = editor.getCursor('anchor');
|
||||
const head = editor.getCursor('head');
|
||||
currentLine = Math.min(anchor.line, head.line) - 1;
|
||||
}
|
||||
|
||||
// Look backward from the current line to find the nearest heading
|
||||
for (let line = currentLine; line >= 0; line--) {
|
||||
|
|
@ -325,7 +341,7 @@ class PasteReformmatterSettingsTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
export function transformMarkdown(
|
||||
markdown: string,
|
||||
settings: {
|
||||
markdownRegexReplacements: Array<{pattern: string, replacement: string}>,
|
||||
markdownRegexReplacements: Array<{ pattern: string, replacement: string }>,
|
||||
contextualCascade: boolean,
|
||||
maxHeadingLevel: number,
|
||||
cascadeHeadingLevels: boolean,
|
||||
|
|
@ -24,25 +24,27 @@ 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 regex_replacement of settings.markdownRegexReplacements) {
|
||||
try {
|
||||
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, '\\');
|
||||
.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;
|
||||
// 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("replaced text")
|
||||
console.log(`regex replacements: ${markdown}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error applying markdown regex replacement: ${error}`);
|
||||
|
|
@ -73,7 +75,7 @@ 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}`);
|
||||
|
|
@ -154,6 +156,8 @@ export function transformMarkdown(
|
|||
appliedTransformations = (originalMarkdown !== markdown);
|
||||
}
|
||||
|
||||
console.log(`processed: ${markdown}`);
|
||||
|
||||
// First handle the special line break markers
|
||||
let preserveLineBreaks = !settings.stripLineBreaks;
|
||||
|
||||
|
|
@ -224,6 +228,7 @@ export function transformMarkdown(
|
|||
appliedTransformations = appliedTransformations || (originalMarkdown !== markdown);
|
||||
}
|
||||
|
||||
console.log(`final: ${markdown}`);
|
||||
return {
|
||||
markdown,
|
||||
appliedTransformations
|
||||
|
|
|
|||
Loading…
Reference in a new issue