mirror of
https://github.com/bramses/obsidian-stack-overflow.git
synced 2026-07-22 06:40:29 +00:00
203 lines
5.6 KiB
TypeScript
203 lines
5.6 KiB
TypeScript
// many thanks to https://github.com/zolrath/obsidian-auto-link-title for in cursor replace code and the inspiration
|
|
import {
|
|
Editor,
|
|
Notice,
|
|
Plugin,
|
|
request,
|
|
} from "obsidian";
|
|
import parse, { HTMLElement } from "node-html-parser";
|
|
import { EditorExtensions } from "editor-enhancements";
|
|
|
|
|
|
|
|
export default class StackOverflowAnswers extends Plugin {
|
|
|
|
extractAnswerId(url: string) {
|
|
try {
|
|
if (url.length === 0) {
|
|
new Notice("[Stack Overflow Answers] - url is empty");
|
|
return "";
|
|
}
|
|
if (url.includes("#"))
|
|
// in form of https://stackoverflow.com/questions/14122919/how-to-hide-show-objects-using-three-js-release-54/14123978#14123978
|
|
return url.split("#").pop();
|
|
|
|
const urlPopped = url.split("/"); // in form of https://stackoverflow.com/a/32232028/3952024
|
|
urlPopped.pop();
|
|
return urlPopped.pop(); // second to last
|
|
} catch (err) {
|
|
new Notice("[Stack Overflow Answers] - Could not extract answer id from url");
|
|
return "";
|
|
}
|
|
}
|
|
|
|
extractParagraphs(html: HTMLElement) {
|
|
const paragraphs: string[] = [];
|
|
|
|
html.querySelectorAll("p").forEach((p) => {
|
|
const linksSelectors = p.querySelectorAll("a");
|
|
const links: string[] = [];
|
|
|
|
if (linksSelectors.length > 0) {
|
|
linksSelectors.forEach((link) => {
|
|
const href = link.getAttribute("href");
|
|
const src = link.innerText;
|
|
links.push(href ? href : src);
|
|
});
|
|
|
|
let idx = 0;
|
|
p.innerHTML = p.innerHTML
|
|
.replace(/<\s*a[^>]*>(.*?)<\s*\/\s*a>/g, (match) => {
|
|
return links[idx++];
|
|
})
|
|
.replace(/<code>/g, "`")
|
|
.replace(/<\/code>/g, "`");
|
|
paragraphs.push(p.innerHTML);
|
|
} else {
|
|
paragraphs.push(
|
|
p.innerHTML
|
|
.replace(/<code>/g, "`")
|
|
.replace(/<\/code>/g, "`")
|
|
);
|
|
}
|
|
});
|
|
|
|
return paragraphs;
|
|
}
|
|
|
|
extractCodeBlocks(html: HTMLElement) {
|
|
const codeBlocks: string[] = [];
|
|
|
|
html.querySelectorAll("pre").forEach((pre) => {
|
|
codeBlocks.push(
|
|
pre.innerText.replace("<code>", "").replace("</code>", "")
|
|
);
|
|
});
|
|
|
|
return codeBlocks;
|
|
}
|
|
|
|
generateMarkdown(paragraphs: string[], codeBlocks: string[], url: string, question: string, questionURL: string) {
|
|
let markdown = "";
|
|
|
|
markdown += '\n\n---'
|
|
|
|
markdown += `\n\n# [${question}](https://stackoverflow.com/${questionURL})\n\n`;
|
|
|
|
paragraphs.forEach((p) => {
|
|
markdown += `> ${p}\n>\n`;
|
|
});
|
|
|
|
codeBlocks.forEach((c) => {
|
|
markdown += `\`\`\`\n${c}\n\`\`\`\n\n`
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/&/g, "&")
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'");
|
|
});
|
|
|
|
markdown += `\n\n[View Answer on Stack Overflow](${url})`;
|
|
|
|
markdown += "\n\n---\n";
|
|
|
|
return markdown;
|
|
}
|
|
|
|
async conveyorBelt(url = "") {
|
|
if (url.length === 0) {
|
|
new Notice("[Stack Overflow Answers] - Nothing Selected");
|
|
return "";
|
|
}
|
|
|
|
if (!url.includes("stackoverflow.com")) {
|
|
new Notice("[Stack Overflow Answers] - Invalid URL");
|
|
return "";
|
|
}
|
|
|
|
const res = await request({
|
|
url: url,
|
|
});
|
|
const root = parse(res, {
|
|
lowerCaseTagName: false, // convert tag name to lower case (hurts performance heavily)
|
|
comment: false, // retrieve comments (hurts performance slightly)
|
|
blockTextElements: {
|
|
script: false, // keep text content when parsing
|
|
noscript: true, // keep text content when parsing
|
|
style: false, // keep text content when parsing
|
|
pre: true, // keep text content when parsing
|
|
},
|
|
});
|
|
|
|
const struct = root
|
|
.getElementById(`answer-${this.extractAnswerId(url)}`)
|
|
.querySelector("div.js-post-body");
|
|
|
|
const question = root.getElementById("question-header").querySelector('a').innerText;
|
|
const questionURL = root.getElementById("question-header").querySelector('a').getAttribute("href");
|
|
|
|
const ps = this.extractParagraphs(struct);
|
|
const cbs = this.extractCodeBlocks(struct);
|
|
|
|
const markdown = this.generateMarkdown(ps, cbs, url, question, questionURL);
|
|
return markdown;
|
|
}
|
|
|
|
// Custom hashid by @shabegom
|
|
private createBlockHash(): string {
|
|
let result = "";
|
|
const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
const charactersLength = characters.length;
|
|
for (let i = 0; i < 4; i++) {
|
|
result += characters.charAt(
|
|
Math.floor(Math.random() * charactersLength)
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async convertUrlToTitledLink(editor: Editor, url: string): Promise<void> {
|
|
// Generate a unique id for find/replace operations for the title.
|
|
const pasteId = `Fetching Answer#${this.createBlockHash()}`;
|
|
|
|
// Instantly paste so you don't wonder if paste is broken
|
|
editor.replaceSelection(`${pasteId}`);
|
|
|
|
// Fetch title from site, replace Fetching Title with actual title
|
|
const stackOverflowMarkdown = await this.conveyorBelt(url);
|
|
|
|
const text = editor.getValue();
|
|
|
|
const start = text.indexOf(pasteId);
|
|
if (start < 0) {
|
|
console.log(
|
|
`Unable to find text "${pasteId}" in current editor, bailing out; link ${url}`
|
|
);
|
|
} else {
|
|
const end = start + pasteId.length;
|
|
const startPos = EditorExtensions.getEditorPositionFromIndex(
|
|
text,
|
|
start
|
|
);
|
|
const endPos = EditorExtensions.getEditorPositionFromIndex(
|
|
text,
|
|
end
|
|
);
|
|
|
|
editor.replaceRange(stackOverflowMarkdown, startPos, endPos);
|
|
}
|
|
}
|
|
|
|
async onload() {
|
|
this.addCommand({
|
|
id: "insert-stack-overflow-answer",
|
|
name: "Insert Stack Overflow Answer",
|
|
editorCallback: async (editor: Editor) => {
|
|
const selectedText = (EditorExtensions.getSelectedText(editor) || "").trim();
|
|
this.convertUrlToTitledLink(editor, selectedText);
|
|
},
|
|
});
|
|
}
|
|
|
|
onunload() {}
|
|
}
|