Refactor reading view and structure

This commit is contained in:
Mayuran Visakan 2023-06-24 02:33:59 +01:00
parent c8ba7dbed7
commit 6ab5a421da
13 changed files with 3795 additions and 3852 deletions

6812
main.js

File diff suppressed because one or more lines are too long

0
src/ApplyStyling.ts Normal file
View file

View file

@ -2,7 +2,7 @@ import { ViewPlugin, Decoration, WidgetType } from "@codemirror/view";
import { RangeSet } from "@codemirror/state";
import { syntaxTree } from "@codemirror/language";
import { splitAndTrimString, searchString, getHighlightedLines, getLanguageIcon, getLanguageName, isExcluded } from "./Utils";
import { searchString, getHighlightedLines, getLanguageIcon, getLanguageName, isExcluded } from "./Utils";
export function codeblockHighlight(settings: CodeblockCustomizerSettings) {
const viewPlugin = ViewPlugin.fromClass(

View file

@ -0,0 +1,49 @@
import { LANGUAGE_NAMES, LANGUAGE_ICONS } from "./LanguageDetails";
import { CodeblockParameters } from "./CodeblockParsing";
import { CodeblockCustomizerThemeSettings } from "./Settings";
export function createHeader(codeblockPreElement: HTMLElement, codeblockParameters: CodeblockParameters, themeSettings: CodeblockCustomizerThemeSettings) {
const headerContainer = createDiv({cls: `codeblock-customizer-header-container${(codeblockParameters.fold.enabled || codeblockParameters.title !== '')?'-specific':''}`});
if (codeblockParameters.language !== ''){
const IconURL = getLanguageIcon(codeblockParameters.language)
if (IconURL !== null) {
const imageWrapper = createDiv();
const img = document.createElement("img");
img.classList.add("codeblock-customizer-icon");
img.width = themeSettings.advanced.iconSize;
img.src = IconURL;
imageWrapper.appendChild(img);
headerContainer.appendChild(imageWrapper);
}
headerContainer.appendChild(createDiv({cls: `codeblock-customizer-header-language-tag-${codeblockParameters.language}`, text: getLanguageTag(codeblockParameters.language)}));
}
let headerText = ''
if (codeblockParameters.title !== '')
headerText = codeblockParameters.title;
else if (codeblockParameters.fold.enabled)
headerText = codeblockParameters.fold.placeholder!==''?codeblockParameters.fold.placeholder:themeSettings.header.collapsePlaceholder;
headerContainer.appendChild(createDiv({cls: "codeblock-customizer-header-text", text: headerText}));
const codeblockPreParent = codeblockPreElement.parentNode;
if (codeblockPreParent)
codeblockPreParent.insertBefore(headerContainer, codeblockPreElement);
headerContainer.addEventListener("click", ()=>{codeblockPreElement.classList.toggle("codeblock-customizer-codeblock-collapsed")});
if (codeblockParameters.fold.enabled)
codeblockPreElement.classList.add(`codeblock-customizer-codeblock-collapsed`);
}
function getLanguageTag(language: string) {
if (language in LANGUAGE_NAMES)
return LANGUAGE_NAMES[language];
else if (language !== '')
return language.charAt(0).toUpperCase() + language.slice(1);
return "";
}
function getLanguageIcon(language: string) {
language = getLanguageTag(language);
if (language in LANGUAGE_ICONS)
return LANGUAGE_ICONS[language];
return null;
}

113
src/CodeblockParsing.ts Normal file
View file

@ -0,0 +1,113 @@
import { CodeblockCustomizerTheme } from "./Settings";
export interface CodeblockParameters {
language: string;
title: string;
fold: {
enabled: boolean;
placeholder: string;
}
lineNumbers: {
alwaysEnabled: boolean;
alwaysDisabled: boolean;
offset: number;
}
highlights: {
default: Array<number>;
alternative: Record<string,Array<number>>
}
}
function parseHighlightedLines(highlightedLinesString: string): Array<number> {
const lineSections = highlightedLinesString.split(',');
return lineSections.map(lineSection => {
if (lineSection.includes('-')) {
const [start,end] = lineSection.split('-').map(num => parseInt(num));
if (start && end)
return Array.from({length:end-start+1}, (_,num) => num + start);
} else {
return parseInt(lineSection);
}
return [];
}).flat();
}
function parseParameterString(parameterString: string, codeblockParameters: CodeblockParameters, theme: CodeblockCustomizerTheme): void {
if (parameterString.startsWith('title:')) {
let titleMatch = /(["'])([^\1]+)\1/.exec(parameterString.slice('title:'.length));
if (titleMatch)
codeblockParameters.title = titleMatch[2].trim();
} else if (parameterString.startsWith('fold:')) {
let foldPlaceholderMatch = /(["'])([^\1]+)\1/.exec(parameterString.slice('fold:'.length));
if (foldPlaceholderMatch) {
codeblockParameters.fold.enabled = true;
codeblockParameters.fold.placeholder = foldPlaceholderMatch[2].trim();
}
} else if (parameterString === 'fold') {
codeblockParameters.fold.enabled = true;
codeblockParameters.fold.placeholder = '';
} else if (parameterString.startsWith('ln:')) {
parameterString = parameterString.slice('ln:'.length)
if (/^\d+$/.test(parameterString)) {
codeblockParameters.lineNumbers.alwaysEnabled = true;
codeblockParameters.lineNumbers.alwaysDisabled = false;
codeblockParameters.lineNumbers.offset = parseInt(parameterString)-1;
} else if (parameterString.toLowerCase() === 'true') {
codeblockParameters.lineNumbers.alwaysEnabled = true;
codeblockParameters.lineNumbers.alwaysDisabled = false;
codeblockParameters.lineNumbers.offset = 0;
} else if (parameterString.toLowerCase() === 'false') {
codeblockParameters.lineNumbers.alwaysEnabled = false;
codeblockParameters.lineNumbers.alwaysDisabled = true;
codeblockParameters.lineNumbers.offset = 0;
}
} else {
let highlightMatch = /^(\w+):((?:\d+-\d+|\d+)(?:,\d+-\d+|,\d+)*)$/.exec(parameterString);
if (highlightMatch) {
let highlights = parseHighlightedLines(highlightMatch[2]);
if (highlightMatch[1] === 'hl')
codeblockParameters.highlights.default = highlights;
else
if (highlightMatch[1] in theme.colors.light.highlights.alternativeHighlights)
codeblockParameters.highlights.alternative[highlightMatch[1]] = highlights;
}
}
}
export function parseCodeblockParameters(parameterLine: string, theme: CodeblockCustomizerTheme): CodeblockParameters {
let codeblockParameters: CodeblockParameters = {
language: '',
title: '',
fold: {
enabled: false,
placeholder: '',
},
lineNumbers: {
alwaysEnabled: false,
alwaysDisabled: false,
offset: 0,
},
highlights: {
default: [],
alternative: {},
},
}
if (parameterLine.startsWith('```')) {
let languageBreak = parameterLine.indexOf(' ');
codeblockParameters.language = parameterLine.slice('```'.length,languageBreak !== -1?languageBreak:parameterLine.length).toLowerCase();
if (languageBreak === -1)
return codeblockParameters;
const parameterStrings = parameterLine.slice(languageBreak+1).match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g);
if (!parameterStrings)
return codeblockParameters;
parameterStrings.forEach((parameterString) => parseParameterString(parameterString,codeblockParameters,theme))
}
return codeblockParameters;
}
function parseRegexExcludedLanguages(excludedLanguagesString: string): Array<RegExp> {
return excludedLanguagesString.split(",").map(regexLanguage => new RegExp(`^${regexLanguage.trim().replace(/\*/g,'.+')}$`,'i'))
}
export function isLanguageExcluded(language: string, excludedLanguagesString: string): boolean {
return parseRegexExcludedLanguages(excludedLanguagesString).some(regexExcludedLanguage => {
if (regexExcludedLanguage.test(language))
return true;
});
}

View file

@ -96,6 +96,11 @@ function createDecorationWidget(textToDisplay: string, languageName: string, spe
widget: new TextAboveCodeblockWidget(textToDisplay, languageName, specificHeader), block: true});
}// createDecorationWidget
const Collapse = StateEffect.define(), UnCollapse = StateEffect.define()
let pluginSettings: CodeblockCustomizerSettings;
@ -152,26 +157,26 @@ class TextAboveCodeblockWidget extends WidgetType {
toDOM(view: EditorView): HTMLElement {
this.view = view;
const container = createContainer(this.specificHeader);
const headerContainer = createContainer(this.specificHeader);
if (this.Lang){
const Icon = getLanguageIcon(this.Lang)
if (Icon) {
container.appendChild(createCodeblockIcon(this.Lang));
headerContainer.appendChild(createCodeblockIcon(this.Lang));
}
container.appendChild(createCodeblockLang(this.Lang));
headerContainer.appendChild(createCodeblockLang(this.Lang));
}
container.appendChild(createFileName(this.text));
headerContainer.appendChild(createFileName(this.text));
this.observer.view = view;
this.observer.observe(container, { attributes: true });
this.observer.observe(headerContainer, { attributes: true });
container.addEventListener("mousedown", event => {
container.setAttribute("data-clicked", "true");
headerContainer.addEventListener("mousedown", event => {
headerContainer.setAttribute("data-clicked", "true");
});
//EditorView.requestMeasure;
return container;
return headerContainer;
}
destroy(dom: HTMLElement) {

View file

@ -1,7 +1,4 @@
// Prism Languages
// https://prismjs.com/plugins/show-language/
export const Languages = {
const PRISM_LANGUAGES: {[key: string]: string} = { // Prism Languages: https://prismjs.com/plugins/show-language/
"none": "Plain text",
"plain": "Plain text",
"plaintext": "Plain text",
@ -277,9 +274,7 @@ export const Languages = {
"yml": "YAML",
"yang": "YANG",
};
// manually generated list from https://prismjs.com/ - 297 languages
export const manualLang = {
const MANUAL_LANGUAGES: {[key: string]: string} = { // Manually generated list from https://prismjs.com/ - 297 languages
"css":"CSS",
"clike":"C-like",
"javascript":"JavaScript",
@ -672,10 +667,8 @@ export const manualLang = {
"yang":"YANG",
"zig":"Zig"
}
// https://github.com/vscode-icons/vscode-icons/wiki/ListOfFiles
// https://devicon.dev/
export const Icons = {
export const LANGUAGE_NAMES: {[key: string]: string} = {...PRISM_LANGUAGES,...MANUAL_LANGUAGES};
const LANGUAGE_ICONS_DATA: {[key: string]: string} = { // Icons from https://github.com/vscode-icons/vscode-icons/wiki/ListOfFiles and https://devicon.dev/
".gitignore": `<path d="M29.472,14.753,17.247,2.528a1.8,1.8,0,0,0-2.55,0L12.158,5.067l3.22,3.22a2.141,2.141,0,0,1,2.712,2.73l3.1,3.1a2.143,2.143,0,1,1-1.285,1.21l-2.895-2.895v7.617a2.141,2.141,0,1,1-1.764-.062V12.3a2.146,2.146,0,0,1-1.165-2.814L10.911,6.314,2.528,14.7a1.8,1.8,0,0,0,0,2.551L14.753,29.472a1.8,1.8,0,0,0,2.55,0L29.472,17.3a1.8,1.8,0,0,0,0-2.551" style="fill:#dd4c35"/><path d="M12.158,5.067l3.22,3.22a2.141,2.141,0,0,1,2.712,2.73l3.1,3.1a2.143,2.143,0,1,1-1.285,1.21l-2.895-2.895v7.617a2.141,2.141,0,1,1-1.764-.062V12.3a2.146,2.146,0,0,1-1.165-2.814L10.911,6.314" style="fill:#fff"/>`,
".hgignore": `<path d="M28.042,23.172c4.989-8.3-1.054-21.751-12.1-20.384C5.955,4.022,5.794,14.53,14.593,17.026c7.614,2.162,1.573,6.992,1.749,10.208s6.62,4.382,11.7-4.063" style="fill:#1b1b1b"/><circle cx="9.784" cy="24.257" r="4.332" style="fill:#1b1b1b"/><circle cx="4.835" cy="15.099" r="2.835" style="fill:#1b1b1b"/><path d="M28.231,22.835c4.989-8.3-1.054-21.751-12.1-20.384C6.144,3.686,5.983,14.194,14.781,16.69c7.614,2.162,1.573,6.992,1.749,10.208s6.62,4.382,11.7-4.063" style="fill:#bfbfbf"/><circle cx="9.972" cy="23.921" r="4.332" style="fill:#bfbfbf"/><circle cx="5.023" cy="14.762" r="2.835" style="fill:#bfbfbf"/><path d="M17.811,28.168a.669.669,0,0,1,.635-.994,7,7,0,0,0,3.7-.746c3.247-1.841,8.244-10.7,5.731-16.285A12.77,12.77,0,0,0,25.049,5.7c-.236-.249-.1-.236.059-.152a10.08,10.08,0,0,1,2.857,3.676,14.578,14.578,0,0,1,1.1,10.279c-.494,1.817-2.2,5.928-4.691,7.706s-5.424,2.8-6.563.955M15.548,16.673c-1.7-.5-3.894-1.208-5.163-2.867A8.088,8.088,0,0,1,8.854,10.49c-.043-.27-.08-.5,0-.558a21.882,21.882,0,0,0,1.688,2.723,6.487,6.487,0,0,0,3.526,2.256,12.383,12.383,0,0,1,3.867,1.37c.739.629.8,1.989.552,2.142s-.759-1.1-2.938-1.749m-8.155,10.4c3.369,3.121,8.439-1.166,6.207-4.954-.251-.425-.576-.749-.469-.423.714,2.178.054,3.9-1.176,4.788a4.063,4.063,0,0,1-4.192.328c-.39-.2-.551.092-.37.261m-3.93-10.16c.018.2.292.458.722.576a2.969,2.969,0,0,0,2.55-.413,2.759,2.759,0,0,0,.81-3.452c-.172-.308-.4-.533-.218-.041A2.68,2.68,0,0,1,6.148,16.53a2.439,2.439,0,0,1-2.1.164c-.391-.119-.6.016-.58.223"/><path d="M19.056,28.407c-.033.389.414.466,1.016.376a6.755,6.755,0,0,0,2.313-.648,9.54,9.54,0,0,0,3.314-2.63c2.662-3.473,3.6-7.582,3.46-8.173A16.172,16.172,0,0,1,27,22.692c-1.888,2.968-3.256,4.548-6.413,5.314-.879.213-1.485-.112-1.529.4m-7-13.5A7.967,7.967,0,0,0,14.6,16.089a12.2,12.2,0,0,1,2.96,1.31c.378.253.618.819.642.317s-.285-.934-.976-1.164a15.274,15.274,0,0,0-2.009-.674c-.485-.1-1.273-.285-1.949-.493-.371-.114-.748-.313-1.214-.483M10.037,27.718c.429-.09,2.924-.736,3.51-2.788.183-.64.215-.511.164-.165a3.8,3.8,0,0,1-3.358,3.123c-.289.03-.668-.1-.315-.17M5.046,17.2a7.991,7.991,0,0,0,1.195-.336,2.383,2.383,0,0,0,1.232-1.741c.064-.505.083-.378.109-.1a2.627,2.627,0,0,1-2.147,2.324c-.2.028-.56.011-.389-.143" style="fill:#fff"/><path d="M27.54,17.446c2.124-6.123-2.321-15.37-11.315-14.258-8.126,1-8.257,9.557-1.1,11.59,8.112,1.228,3.227,7.347,2.535,10.433-.621,2.766,6.555,3.221,9.876-7.765M7.219,26.2a2.028,2.028,0,0,1,1.332.442,3.525,3.525,0,0,0,3.755-.983A4.154,4.154,0,0,0,12.869,22c-.806-2.319-4.229-2.278-5.758-.353-1.654,2.15-.4,4.539.108,4.548M2.676,15.451a1.166,1.166,0,0,0,.908.863c.731.1.88.434,1.743.263A2.464,2.464,0,0,0,7.1,14.916a1.771,1.771,0,0,0-.824-2.14,2.689,2.689,0,0,0-3.047.363,2.263,2.263,0,0,0-.558,2.312" style="fill:#999"/><path d="M21.981,22.228c-2.2-.272-5.36,4.69-2.378,4.109h0a5.645,5.645,0,0,0,3.683-1.932,23.136,23.136,0,0,0,4.055-7.2c.5-1.861.251-4.745-.269-2.036-.533,2.781-2.893,7.336-5.091,7.064M10.523,26.362A2.778,2.778,0,0,0,12.5,22.99c-.165-1.276-.861,1.584-2.15,2.012-1.953.648-1.733,1.861.176,1.361m-4.978-10.2c.663-.173,1.54-1.077,1.1-1.767-.537-.85-2.033-.122-2.084.824s.277,1.127.979.943" style="fill:#f3f3f3"/>`,
".npmignore": `<path d="M2,10.555H30v9.335H16v1.556H9.778V19.889H2Zm1.556,7.779H6.667V13.666H8.222v4.667H9.778V12.111H3.556Zm7.778-6.223v7.779h3.111V18.334h3.111V12.111Zm3.111,1.556H16v3.112H14.444Zm4.667-1.556v6.223h3.111V13.666h1.556v4.667h1.556V13.666h1.556v4.667h1.556V12.111Z" style="fill:#cb3837"/>`,
@ -853,3 +846,7 @@ export const Icons = {
"YANG": `<path d="M8.877,23.159c0-5.535,3.992-7.168,7.894-7.168,3.357,0,5.988-3.811,5.988-6.624,0-3.621-2.487-5.831-4.882-7.12A13.881,13.881,0,1,0,14.5,29.8C10.491,28.248,8.877,25.324,8.877,23.159Z" style="fill:#fff"/><path d="M14.482,29.917A14,14,0,0,1,16,2a14.154,14.154,0,0,1,1.893.131l.04.013c2.255,1.213,4.944,3.452,4.944,7.223,0,2.715-2.564,6.741-6.106,6.741-2.9,0-7.776.916-7.776,7.05,0,2.022,1.451,4.946,5.542,6.531ZM16,2.236A13.765,13.765,0,0,0,13.637,29.56c-3.581-1.684-4.877-4.447-4.877-6.4,0-6.576,5.6-7.286,8.012-7.286,3.406,0,5.87-3.886,5.87-6.506,0-3.645-2.606-5.82-4.8-7.006A13.928,13.928,0,0,0,16,2.236Z" style="fill:#231f20"/><path d="M29.882,16a13.882,13.882,0,0,0-12-13.752c2.4,1.289,4.882,3.5,4.882,7.12,0,2.813-2.631,6.624-5.988,6.624-3.9,0-7.894,1.633-7.894,7.168,0,2.166,1.613,5.089,5.618,6.641A13.875,13.875,0,0,0,29.882,16Z" style="fill:#231f20"/><path d="M16,30a14.2,14.2,0,0,1-1.518-.083l-.03-.007c-4.2-1.628-5.693-4.654-5.693-6.75,0-6.576,5.6-7.286,8.012-7.286,3.406,0,5.87-3.886,5.87-6.506,0-3.655-2.621-5.833-4.82-7.016l.072-.221A14,14,0,0,1,16,30Zm-1.477-.316A13.756,13.756,0,0,0,29.764,16,13.807,13.807,0,0,0,18.5,2.466c2.115,1.272,4.377,3.441,4.377,6.9,0,2.715-2.564,6.741-6.106,6.741-2.9,0-7.776.916-7.776,7.05C9,25.178,10.443,28.1,14.523,29.684Z" style="fill:#231f20"/><circle cx="15.943" cy="22.787" r="1.506" style="fill:#fff"/><circle cx="16.007" cy="9.142" r="1.506" style="fill:#231f20"/>`,
"Zig": `<polygon points="5.733 19.731 5.733 12.264 8.533 12.264 8.533 8.531 2 8.531 2 23.464 5.547 23.464 8.907 19.731 5.733 19.731" style="fill:#f7a41d"/><polygon points="26.453 8.531 23.093 12.264 26.267 12.264 26.267 19.731 23.467 19.731 23.467 23.464 30 23.464 30 8.531 26.453 8.531" style="fill:#f7a41d"/><polygon points="26.875 6.707 20.513 8.531 9.467 8.531 9.467 12.264 16.847 12.264 5.115 25.293 11.497 23.464 22.533 23.464 22.533 19.731 15.148 19.731 26.875 6.707" style="fill:#f7a41d"/>`
}
export const LANGUAGE_ICONS: {[key: string]: string} = Object.keys(LANGUAGE_ICONS_DATA).reduce((result: {[key: string]: string}, key: string) => {
result[key] = URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32">${LANGUAGE_ICONS_DATA[key]}</svg>`], { type: "image/svg+xml" }));
return result
},{})

View file

@ -1,7 +1,109 @@
import { MarkdownView, MarkdownPostProcessorContext, sanitizeHTMLToDom } from "obsidian";
import { TFile, MarkdownView, MarkdownPostProcessorContext, MarkdownSectionInformation, CachedMetadata, sanitizeHTMLToDom } from "obsidian";
import { searchString, getHighlightedLines, getLanguageName, isExcluded, getLanguageIcon, createContainer, createCodeblockLang, createCodeblockIcon, createFileName } from "./Utils";
import CodeblockCustomizerPlugin from "./main";
import { CodeblockCustomizerThemeSettings } from "./Settings";
import { CodeblockParameters, parseCodeblockParameters, isLanguageExcluded } from "./CodeblockParsing";
import { createHeader } from "./CodeblockDecorating";
export async function ReadingView(codeBlockElement: HTMLElement, context: MarkdownPostProcessorContext, plugin: CodeblockCustomizerPlugin) {
export async function readingViewPostProcessor(element: HTMLElement, context: MarkdownPostProcessorContext, plugin: CodeblockCustomizerPlugin) {
const codeblockCodeElement: HTMLElement | null = element.querySelector('pre > code');
if (!codeblockCodeElement)
return;
if (Array.from(codeblockCodeElement.classList).some(className => /^language-\S+/.test(className)))
while(!codeblockCodeElement.classList.contains("is-loaded"))
await sleep(2);
const codeblockPreElement: HTMLPreElement | null = element.querySelector("pre:not(.frontmatter)");
if (!codeblockPreElement)
return;
const codeblockSectionInfo: MarkdownSectionInformation | null = context.getSectionInfo(codeblockCodeElement);
if (codeblockSectionInfo) {
const view: MarkdownView | null = plugin.app.workspace.getActiveViewOfType(MarkdownView);
if (!view || typeof view?.editor === 'undefined')
return;
const codeblockFirstLine = view.editor.getLine(codeblockSectionInfo.lineStart);
remakeCodeblock(codeblockCodeElement, codeblockPreElement, codeblockFirstLine, plugin);
} else {
// PDF export
const file = plugin.app.vault.getAbstractFileByPath(context.sourcePath);
if (!file) {
console.error(`File not found: ${context.sourcePath}`);
return;
}
const fileContent = await plugin.app.vault.cachedRead(<TFile> file).catch((error) => {
console.error(`Error reading file: ${error.message}`);
return '';
});
}
const cache: CachedMetadata | null = plugin.app.metadataCache.getCache(context.sourcePath);
const fileContentLines = fileContent.split(/\n/g);
const codeBlockFirstLines: Array<string> = [];
if (typeof cache?.sections !== 'undefined') {
for (const element of cache.sections)
if (element.type === 'code')
codeBlockFirstLines.push(fileContentLines[element.position.start.line]);
} else {
console.error(`Metadata cache not found for file: ${context.sourcePath}`);
return;
}
try {
PDFExport(element, plugin, codeBlockFirstLines);
} catch (error) {
console.error(`Error exporting to PDF: ${error.message}`);
return;
}
}
}
function decorateCodeblock(codeblockCodeElement: HTMLElement, codeblockPreElement: HTMLElement, codeblockParameters: CodeblockParameters, themeSettings: CodeblockCustomizerThemeSettings) {
createHeader(codeblockPreElement,codeblockParameters,themeSettings)
let codeblockLines = codeblockCodeElement.innerHTML.split("\n");
if (codeblockLines.length == 1)
codeblockLines = ['',''];
codeblockCodeElement.innerHTML = "";
codeblockLines.forEach((line,index) => {
const lineNumber = index + 1;
const lineWrapper = document.createElement("div");
//TODO (@mayurankv) Future: Speed this up by setting up a reverse dictionary for line number to highlights
if (codeblockParameters.highlights.default.includes(lineNumber))
lineWrapper.classList.add('codeblock-customizer-line-highlighted')
Object.entries(codeblockParameters.highlights.alternative).forEach(([alternativeHighlight,highlightedLineNumbers]: [string,Array<number>]) => {
if (highlightedLineNumbers.includes(lineNumber))
lineWrapper.classList.add(`codeblock-customizer-line-highlighted-${alternativeHighlight.replace(/\s+/g, '-').toLowerCase()}`)
})
codeblockCodeElement.appendChild(lineWrapper);
let lineNumberDisplay = '';
if (!codeblockParameters.lineNumbers.alwaysEnabled && codeblockParameters.lineNumbers.alwaysDisabled)
lineNumberDisplay = '-hide'
else if (codeblockParameters.lineNumbers.alwaysEnabled && !codeblockParameters.lineNumbers.alwaysDisabled)
lineNumberDisplay = '-specific'
lineWrapper.appendChild(createDiv({cls: `codeblock-customizer-line-number${lineNumberDisplay}`, text: lineNumber.toString()}));
lineWrapper.appendChild(createDiv({cls: `codeblock-customizer-line-text`, text: sanitizeHTMLToDom(line !== "" ? line : "<br>")}));
})
}
function remakeCodeblock(codeblockCodeElement: HTMLElement, codeblockPreElement: HTMLElement, codeblockFirstLine: string, plugin: CodeblockCustomizerPlugin) {
const codeblockParameters = parseCodeblockParameters(codeblockFirstLine,plugin.settings.currentTheme);
const excludeCodeblock = isLanguageExcluded(codeblockParameters.language,plugin.settings.excludedLanguages);
if (excludeCodeblock)
return;
codeblockPreElement.classList.add(`codeblock-customizer-pre`);
if (codeblockPreElement.parentElement)
codeblockPreElement.parentElement.classList.add(`codeblock-customizer-pre-parent`);
decorateCodeblock(codeblockCodeElement,codeblockPreElement,codeblockParameters,plugin.settings.currentTheme.settings);
}
function PDFExport(element: HTMLElement, plugin: CodeblockCustomizerPlugin, codeBlockFirstLines: string[]) {
const codeblockPreElements = element.querySelectorAll('pre:not(.frontmatter)');
codeblockPreElements.forEach((codeblockPreElement: HTMLElement, key: number) => {
const codeblockCodeElement: HTMLPreElement | null = codeblockPreElement.querySelector("pre > code");
if (!codeblockCodeElement)
return;
const codeblockFirstLine = codeBlockFirstLines[key];
remakeCodeblock(codeblockCodeElement, codeblockPreElement, codeblockFirstLine, plugin)
})
}

View file

@ -1,230 +0,0 @@
import { MarkdownView, MarkdownPostProcessorContext, sanitizeHTMLToDom } from "obsidian";
import { searchString, getHighlightedLines, getLanguageName, isExcluded, getLanguageIcon, createContainer, createCodeblockLang, createCodeblockIcon, createFileName } from "./Utils";
export async function ReadingView(codeBlockElement: HTMLElement, context: MarkdownPostProcessorContext, plugin: CodeblockCustomizerPlugin) {
const codeElm: HTMLElement = codeBlockElement.querySelector('pre > code');
if (!codeElm)
return;
const classRegex = /^language-\S+/;
const match = Array.from(codeElm.classList).some(className => classRegex.test(className));
if (match)
while(!codeElm.classList.contains("is-loaded"))
await sleep(2);
const codeBlockSectionInfo = context.getSectionInfo(codeElm);
let codeBlockFirstLine = "";
if (codeBlockSectionInfo) {
const view = plugin.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.editor)
codeBlockFirstLine = view.editor.getLine(codeBlockSectionInfo.lineStart);
} else {
// PDF export
const file = plugin.app.vault.getAbstractFileByPath(context.sourcePath);
if (!file) {
//console.error(`File not found: ${context.sourcePath}`);
return;
}
const cache = plugin.app.metadataCache.getCache(context.sourcePath);
const fileContent = await plugin.app.vault.cachedRead(<TFile> file).catch((error) => {
//console.error(`Error reading file: ${error.message}`);
return '';
});
const fileContentLines = fileContent.split(/\n/g);
const codeBlockFirstLines: string[] = [];
if (cache.sections) {
for (const element of cache.sections) {
if (element.type === 'code') {
const lineStart = element.position.start.line;
const codeBlockFirstLine = fileContentLines[lineStart];
if (!isAdmonition(codeBlockFirstLine)) {
codeBlockFirstLines.push(codeBlockFirstLine);
}
}
}
} else {
//console.error(`Metadata cache not found for file: ${context.sourcePath}`);
return;
}
try {
await PDFExport(codeBlockElement, plugin, codeBlockFirstLines);
} catch (error) {
//console.error(`Error exporting to PDF: ${error.message}`);
return;
}
return;
}
const codeBlockLang = searchString(codeBlockFirstLine, "```");
const highlightedLinesParams = searchString(codeBlockFirstLine, "HL:");
const linesToHighlight = getHighlightedLines(highlightedLinesParams);
const FileName = searchString(codeBlockFirstLine, "file:");
const Fold = searchString(codeBlockFirstLine, "fold");
const lineNumberOffset = 0; //searchString(codeBlockFirstLine, "ln:")//TODO (@mayurankv) Set line number offset here - Should be ln_value - 1 since it is offset, not starting line number - 0 if true OR false
const showNumbers = true; //TODO (@mayurankv) Set showNumbers to be true if ln:<number> or ln:true or false if ln:false
const alternateColors = plugin.settings.alternateColors || [];
let altHL = [];
for (const { name } of alternateColors) {
const altParams = searchString(codeBlockFirstLine, `${name}:`);
altHL = altHL.concat(getHighlightedLines(altParams).map((lineNumber) => ({ name, lineNumber })));
}
let isCodeBlockExcluded = false;
isCodeBlockExcluded = isExcluded(codeBlockFirstLine, plugin.settings.ExcludeLangs);
const codeElements = codeBlockElement.getElementsByTagName("code");
const codeBlockPreElement: HTMLPreElement | null = codeBlockElement.querySelector("pre:not(.frontmatter)");
if (codeBlockPreElement === null) {
return;
}
if (!isCodeBlockExcluded) {
codeBlockPreElement.classList.add(`codeblock-customizer-pre`);
}
AddHeaderAndHighlight(isCodeBlockExcluded, FileName, codeBlockPreElement, codeBlockLang, Fold, codeElements, showNumbers, lineNumberOffset, linesToHighlight, altHL );
}// ReadingView
function isAdmonition(lineText: string): boolean {
const adTypes = ["ad-note", "ad-seealso", "ad-abstract", "ad-summary", "ad-tldr", "ad-info", "ad-todo", "ad-tip", "ad-hint", "ad-important", "ad-success", "ad-check", "ad-done", "ad-question", "ad-help", "ad-faq", "ad-warning", "ad-caution", "ad-attention", "ad-failure", "ad-fail", "ad-missing", "ad-danger", "ad-error", "ad-bug", "ad-example", "ad-quote", "ad-cite"]; //TODO (@mayurankv) refactor to use regex
const codeBlockLang = searchString(lineText, "```");
return adTypes.some((adType) => codeBlockLang && codeBlockLang.startsWith(adType));
}// isAdmonition
function HeaderWidget(preElements, textToDisplay: string, specificHeader: boolean, codeblockLanguage: string, Collapse: boolean) {
const parent = preElements.parentNode;
const container = createContainer(specificHeader);
if (codeblockLanguage){
const Icon = getLanguageIcon(codeblockLanguage)
if (Icon) {
container.appendChild(createCodeblockIcon(codeblockLanguage));
}
container.appendChild(createCodeblockLang(codeblockLanguage));
}
container.appendChild(createFileName(textToDisplay));
parent.insertBefore(container, preElements);
// Add event listener to the widget element
container.addEventListener("click", function() {
// Toggle the "collapsed" class on the codeblock element
preElements.classList.toggle("codeblock-customizer-codeblock-collapsed");
});
if (Collapse) {
preElements.classList.add(`codeblock-customizer-codeblock-collapsed`);
}
}// HeaderWidget
function createLineNumberElement(lineNumber,showNumbers) {
const lineNumberWrapper = document.createElement("div");
lineNumberWrapper.classList.add(`codeblock-customizer-line-number${showNumbers?'':'-hide'}`);
lineNumberWrapper.setText(lineNumber);
return lineNumberWrapper;
}// createLineNumberElement
function createLineTextElement(line) {
const lineText = line !== "" ? line : "<br>";
const sanitizedText = sanitizeHTMLToDom(lineText);
const lineContentWrapper = createDiv({cls: `codeblock-customizer-line-text`, text: sanitizedText});
return lineContentWrapper;
}// createLineTextElement
function highlightLines(codeElements, showNumbers, lineNumberOffset, linesToHighlight, altHL) {
for (let i = 0; i < codeElements.length; i++) {
let lines = codeElements[i].innerHTML.split("\n");
const preElm = codeElements[i].parentNode;
if (preElm === null || preElm.nodeName !== "PRE") // only process pre > code elements
return;
if (lines.length == 1) {
lines = ['',''];
}
codeElements[i].innerHTML = "";
for (let j = 0; j < lines.length - 1; j++) {
const line = lines[j];
const lineNumber = j + 1;
const isHighlighted = linesToHighlight.includes(lineNumber);
const altHLMatch = altHL.filter((hl) => hl.lineNumber === lineNumber);
// create line element
const lineWrapper = document.createElement("div");
if (isHighlighted) {
lineWrapper.classList.add(`codeblock-customizer-line-highlighted`);
}
else if (altHLMatch.length > 0) {
lineWrapper.classList.add(`codeblock-customizer-line-highlighted-${altHLMatch[0].name.replace(/\s+/g, '-').toLowerCase()}`);
}
codeElements[i].appendChild(lineWrapper);
// create line number element
const lineNumberEl = createLineNumberElement(lineNumber+lineNumberOffset,showNumbers);
lineWrapper.appendChild(lineNumberEl);
// create line text element
const lineTextEl = createLineTextElement(line);
lineWrapper.appendChild(lineTextEl);
}
}
}// highlightLines
function AddHeaderAndHighlight(isCodeBlockExcluded, fileName, codeBlockPreElement, codeBlockLang, Fold, codeElements, showNumbers, lineNumberOffset, linesToHighlight, altHL ){
if (!isCodeBlockExcluded) {
let specificHeader = true;
if (fileName === null || fileName === "") {
if (Fold) {
fileName = 'Collapsed Code';
} else {
fileName = '';
specificHeader = false;
}
}
HeaderWidget(codeBlockPreElement, fileName, specificHeader, getLanguageName(codeBlockLang), Fold);
highlightLines(codeElements, showNumbers, lineNumberOffset, linesToHighlight, altHL);
if (codeBlockPreElement.parentElement) {
codeBlockPreElement.parentElement.classList.add(`codeblock-customizer-pre-parent`);
}
}
}// AddHeaderAndHighlight
function PDFExport(codeBlockElement: HTMLElement, plugin: CodeblockCustomizerPlugin, codeBlockFirstLines: string[]) {
const codeBlocks = codeBlockElement.querySelectorAll('pre > code');
codeBlocks.forEach((codeElm, key) => {
const codeBlockFirstLine = codeBlockFirstLines[key];
const codeBlockLang = searchString(codeBlockFirstLine, "```");
const highlightedLinesParams = searchString(codeBlockFirstLine, "HL:");
const linesToHighlight = getHighlightedLines(highlightedLinesParams);
const fileName = searchString(codeBlockFirstLine, "file:");
const Fold = searchString(codeBlockFirstLine, "fold");
const lineNumberOffset = 0; //searchString(codeBlockFirstLine, "ln:")//TODO (@mayurankv) Set line number offset here - Should be ln_value - 1 since it is offset, not starting line number - 0 if true OR false
const showNumbers = true; //TODO (@mayurankv) Set showNumbers to be true if ln:<number> or ln:true or false if ln:false
const alternateColors = plugin.settings.alternateColors || [];
let altHL = [];
for (const { name, _ } of alternateColors) {
const altParams = searchString(codeBlockFirstLine, `${name}:`);
altHL = altHL.concat(getHighlightedLines(altParams).map((lineNumber) => ({ name, lineNumber })));
}
let isCodeBlockExcluded = false;
isCodeBlockExcluded = isExcluded(codeBlockFirstLine, plugin.settings.ExcludeLangs);
const codeBlockPreElement: HTMLPreElement | null = codeElm.parentElement;
if (codeBlockPreElement === null) {
return;
}
if (!isCodeBlockExcluded){
codeBlockPreElement.classList.add(`codeblock-customizer-pre`);
}
const codeElements = codeBlockPreElement.getElementsByTagName("code");
AddHeaderAndHighlight(isCodeBlockExcluded, fileName, codeBlockPreElement, codeBlockLang, Fold, codeElements, showNumbers, lineNumberOffset, linesToHighlight, altHL );
})
}// PDFExport

View file

@ -92,7 +92,7 @@ export interface CodeblockCustomizerSettings {
default: boolean;
},
newHighlight: string;
excludedLangs: string;
excludedLanguages: string;
}
// Theme Defaults
@ -244,5 +244,5 @@ export const DEFAULT_SETTINGS: CodeblockCustomizerSettings = {
currentTheme: structuredClone(DEFAULT_THEME),
newTheme: NEW_THEME_DEFAULT,
newHighlight: '',
excludedLangs: "dataview, dataviewjs, ad-*",
excludedLanguages: "dataview, dataviewjs, ad-*",
}

View file

@ -49,9 +49,9 @@ export class SettingsTab extends PluginSettingTab {
.setDesc('Define languages in a comma separated list on which the plugin should not apply. You can use a wildcard (*) either at the beginning, or at the end. For example: ad-* will exclude codeblocks where the language starts with ad- e.g.: ad-info, ad-error etc.')
.addText(text => text
.setPlaceholder('e.g. dataview, python etc.')
.setValue(this.plugin.settings.excludedLangs)
.setValue(this.plugin.settings.excludedLanguages)
.onChange((value) => {
this.plugin.settings.excludedLangs = value;
this.plugin.settings.excludedLanguages = value;
(async () => {await this.plugin.saveSettings()})();
}));
@ -560,15 +560,12 @@ export class SettingsTab extends PluginSettingTab {
// ========== Donation ==========
const donationDiv = containerEl.createEl("div", { cls: "codeblock-customizer-donation", });
const donationText = createEl("p");
const donationButton = createEl("a", { href: "https://www.buymeacoffee.com/ThePirateKing"});
donationText.appendText("If you like this plugin, and would like to help support continued development, use the button below!");
donationButton.innerHTML = `<img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=ThePirateKing&button_colour=e3e7ef&font_colour=262626&font_family=Inter&outline_colour=262626&coffee_colour=ff0000" height="42px">`;
const donationText = createEl("p", {text: "If you like this plugin, and would like to help support continued development, use the button below!"});
donationDiv.appendChild(donationText);
const donationButton = createEl("a", { href: "https://www.buymeacoffee.com/ThePirateKing"});
donationButton.innerHTML = `<img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=ThePirateKing&button_colour=e3e7ef&font_colour=262626&font_family=Inter&outline_colour=262626&coffee_colour=ff0000" height="42px">`;
donationDiv.appendChild(donationButton);
}//display
}
// Setting Creation
createPickr(plugin: CodeblockCustomizerPlugin, containerEl: HTMLElement, setting: Setting, id: string, getRelevantThemeColor: (relevantThemeColors: CodeblockCustomizerThemeColors)=>Color, saveRelevantThemeColor: (relevantThemeColors: CodeblockCustomizerThemeColors, saveColor: Color)=>void, disabled?: ()=>boolean) {
@ -675,7 +672,7 @@ export class SettingsTab extends PluginSettingTab {
})
})
}
}// SettingsTab
}
class PickrResettable extends Pickr {
saveColor: (saveColor: Color)=>void;

View file

@ -1,194 +1,43 @@
import { Languages, manualLang, Icons } from "./Const";
import { CodeblockCustomizerSettings } from "./Settings";
export function splitAndTrimString(str) {
if (!str) {
return [];
}
// Replace * with .*
str = str.replace(/\*/g, '.*');
if (!str.includes(",")) {
return [str];
}
return str.split(",").map(s => s.trim());
}// splitAndTrimString
export function searchString(str, searchTerm) {
const originalStr = str;
str = str.toLowerCase();
searchTerm = searchTerm.toLowerCase();
if (searchTerm === "file:") {
if (str.includes(searchTerm)) {
const startIndex = str.indexOf(searchTerm) + searchTerm.length;
let result = "";
if (str[startIndex] === "\"") {
const endIndex = str.indexOf("\"", startIndex + 1);
if (endIndex !== -1) {
result = originalStr.substring(startIndex + 1, endIndex);
} else {
result = originalStr.substring(startIndex + 1);
}
} else {
const endIndex = str.indexOf(" ", startIndex);
if (endIndex !== -1) {
result = originalStr.substring(startIndex, endIndex);
} else {
result = originalStr.substring(startIndex);
}
}
return result.trim();
}
} else if (searchTerm === "```") {
if (str.startsWith(searchTerm)) {
const startIndex = searchTerm.length;
const endIndex = str.indexOf(" ", startIndex);
let word = "";
if (endIndex !==-1) {
word = originalStr.substring(startIndex, endIndex);
} else {
word = originalStr.substring(startIndex);
}
if (!word.includes(":")) {
if (word.toLowerCase() === "fold")
return null;
else
return word;
}
}
} else if (searchTerm === 'fold') {
if (str.includes(" fold ")) {
return true;
}
const index = str.indexOf(searchTerm);
if (index !== -1 && index === str.length - searchTerm.length && str[index - 1] === " ") {
return true;
}
if (str.includes("```fold ")) {
return true;
}
if (str.includes("```fold") && str.indexOf("```fold") + "```fold".length === str.length) {
return true;
}
return false;
} else {
if (str.includes(searchTerm)) {
const startIndex = str.indexOf(searchTerm) + searchTerm.length;
const endIndex = str.indexOf(" ", startIndex);
if (endIndex !== -1) {
return originalStr.substring(startIndex, endIndex).trim();
} else {
return originalStr.substring(startIndex).trim();
}
// Setting Management
export function getCurrentMode() {
const body = document.querySelector('body');
if (body !== null){
if (body.classList.contains('theme-light')) {
return "light";
} else if (body.classList.contains('theme-dark')) {
return "dark";
}
}
return null;
}//searchString
console.log('Warning: Couldn\'t get current theme');
return "light";
}
export function getHighlightedLines(params: string): number[] {
if (!params) {
return [];
}
const trimmedParams = params.trim();
const lines = trimmedParams.split(",");
return lines.map(line => {
if (line.includes("-")) {
const range = line.split("-");
const start = parseInt(range[0], 10);
const end = parseInt(range[1], 10);
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
return parseInt(line, 10);
}).flat();
}// getHighlightedLines
export function isExcluded(lineText: string, excludeLangs: string[]) : boolean {
const codeBlockLang = searchString(lineText, "```");
const regexLangs = splitAndTrimString(excludeLangs).map(lang => new RegExp(`^${lang.replace(/\*/g, '.*')}$`, 'i'));
for (const regexLang of regexLangs) {
if (codeBlockLang && regexLang.test(codeBlockLang)) {
return true;
}
}
return false;
}// isExcluded
export function getLanguageIcon(DisplayName) {
if (!DisplayName)
return "";
if (Icons.hasOwnProperty(DisplayName)) {
return Icons[DisplayName];
}
return null;
}// getLanguageIcon
export function getLanguageName(code) {
if (!code)
return "";
code = code.toLowerCase();
if (Languages.hasOwnProperty(code)) {
return Languages[code];
} else if (manualLang.hasOwnProperty(code)) {
return manualLang[code];
} else if (code){
return code.charAt(0).toUpperCase() + code.slice(1);
}
return "";
}// getLanguageName
export const BLOBS: Record<string, string> = {};
export function loadIcons(){
for (const [key, value] of Object.entries(Icons)) {
BLOBS[key.replace(/\s/g, "_")] = URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32">${value}</svg>`], { type: "image/svg+xml" }));
}
}// loadIcons
// Functions for displaying header BEGIN
export function createContainer(specific: boolean) {
const container = document.createElement("div");
container.classList.add(`codeblock-customizer-header-container${specific?'-specific':''}`);
return container;
}// createContainer
export function createCodeblockLang(lang: string) {
const codeblockLang = document.createElement("div");
codeblockLang.innerText = lang;
codeblockLang.classList.add(`codeblock-customizer-header-language-tag-${getLanguageName(lang).toLowerCase()}`);
return codeblockLang;
}// createCodeblockLang
export function createCodeblockIcon(displayLang: string) {
const div = document.createElement("div");
const img = document.createElement("img");
img.classList.add("codeblock-customizer-icon");
img.width = 28; //32
img.src = BLOBS[displayLang.replace(/\s/g, "_")];
div.appendChild(img);
return div;
}// createCodeblockIcon
export function createFileName(text: string) {
const fileName = document.createElement("div");
fileName.innerText = text;
fileName.classList.add("codeblock-customizer-header-text");
return fileName;
}// createFileName
// Functions for displaying header END
const stylesDict = {
activeCodeBlockLineColor: 'codeblock-active-line-color',
@ -339,39 +188,3 @@ function accessSetting(key: string, settings: CodeblockCustomizerSettings) {
return null;
}
}
// Setting Management
export function getCurrentMode() {
const body = document.querySelector('body');
if (body !== null){
if (body.classList.contains('theme-light')) {
return "light";
} else if (body.classList.contains('theme-dark')) {
return "dark";
}
}
console.log('Warning: Couldn\'t get current theme');
return "light";
}

View file

@ -1,11 +1,12 @@
import { Plugin } from "obsidian";
import { DEFAULT_SETTINGS, CodeblockCustomizerSettings } from './Settings';
import { LANGUAGE_ICONS } from './LanguageDetails';
import { codeblockHighlight } from "./CodeBlockHighlight";
import { codeblockHeader, collapseField } from "./Header";
import { ReadingView } from "./ReadingViewOld";
import { readingViewPostProcessor } from "./ReadingView";
import { SettingsTab } from "./SettingsTab";
import { loadIcons, BLOBS, updateSettingStyles } from "./Utils";
import { } from "./Utils";
export default class CodeblockCustomizerPlugin extends Plugin {
settings: CodeblockCustomizerSettings;
@ -13,43 +14,37 @@ export default class CodeblockCustomizerPlugin extends Plugin {
async onload() {
await this.loadSettings();
document.body.classList.add('codeblock-customizer');
loadIcons();
const settingsTab = new SettingsTab(this.app, this);
this.addSettingTab(settingsTab);
codeblockHeader.settings = this.settings;
collapseField.pluginSettings = this.settings;
this.registerEditorExtension([
codeblockHeader,
collapseField,
codeblockHighlight(this.settings)
]);
const settingsTab = new SettingsTab(this.app, this);
this.addSettingTab(settingsTab);
//updateSettingStyles(this.settings);
this.registerEvent(this.app.workspace.on('css-change', this.handleCssChange.bind(this, settingsTab), this));
// reading mode
this.registerMarkdownPostProcessor(async (el, ctx) => {
await ReadingView(el, ctx, this)
await readingViewPostProcessor(el, ctx, this)
})
//updateSettingStyles(this.settings);
// this.registerEvent(this.app.workspace.on('css-change', this.handleCssChange.bind(this, settingsTab), this));
console.log("loading CodeBlock Customizer plugin");
}// onload
}
handleCssChange(settingsTab) {
console.log('updating css');
}// handleCssChange
// handleCssChange(settingsTab) {
// console.log('updating css');
// }
onunload() {
console.log("unloading CodeBlock Customizer plugin");
// unload icons
for (const url of Object.values(BLOBS)) {
URL.revokeObjectURL(url)
}
for (const url of Object.values(LANGUAGE_ICONS))
URL.revokeObjectURL(url) // Unload icons
}
async loadSettings() {