1
0
Fork 0
digital-art-composition-book/.obsidian/plugins/multi-column-markdown/main.js

6556 lines
1 MiB
JavaScript
Raw Permalink Normal View History

2024-09-14 15:25:13 +00:00
/*
THIS IS A GENERATED/BUNDLED FILE BY ROLLUP
if you want to view the source visit the plugins github repository
*/
'use strict';
var obsidian = require('obsidian');
var state = require('@codemirror/state');
var view = require('@codemirror/view');
var language = require('@codemirror/language');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function getStyle(element, property) {
var _a;
const view = ((_a = element.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView) || window;
const style = view.getComputedStyle(element);
return (style.getPropertyValue(property) || style[property]);
}
const PIXELS_PER_INCH = 96;
const MILLIMETRES_PER_INCH = 25.4;
const POINTS_PER_INCH = 72;
const PICAS_PER_INCH = 6;
function fontSize(element) {
return element
? getStyle(element, 'fontSize') || fontSize(element.parentElement)
: getStyle(window.document.documentElement, 'fontSize');
}
function parse(providedLength) {
var _a;
const length = providedLength || '0';
const value = parseFloat(length);
const match = length.match(/[\d-.]+(\w+)$/);
const unit = (_a = match === null || match === void 0 ? void 0 : match[1]) !== null && _a !== void 0 ? _a : '';
return [value, unit.toLowerCase()];
}
function pixels(length, element) {
var _a, _b;
const view = (_b = (_a = element === null || element === void 0 ? void 0 : element.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView) !== null && _b !== void 0 ? _b : window;
const root = view.document.documentElement || view.document.body;
const [value, unit] = parse(length);
switch (unit) {
case 'rem':
return value * pixels(fontSize(window.document.documentElement));
case 'em':
return value * pixels(fontSize(element), element === null || element === void 0 ? void 0 : element.parentElement);
case 'in':
return value * PIXELS_PER_INCH;
case 'q':
return (value * PIXELS_PER_INCH) / MILLIMETRES_PER_INCH / 4;
case 'mm':
return (value * PIXELS_PER_INCH) / MILLIMETRES_PER_INCH;
case 'cm':
return (value * PIXELS_PER_INCH * 10) / MILLIMETRES_PER_INCH;
case 'pt':
return (value * PIXELS_PER_INCH) / POINTS_PER_INCH;
case 'pc':
return (value * PIXELS_PER_INCH) / PICAS_PER_INCH;
case 'vh':
return (value * view.innerHeight || root.clientWidth) / 100;
case 'vw':
return (value * view.innerWidth || root.clientHeight) / 100;
case 'vmin':
return ((value *
Math.min(view.innerWidth || root.clientWidth, view.innerHeight || root.clientHeight)) /
100);
case 'vmax':
return ((value *
Math.max(view.innerWidth || root.clientWidth, view.innerHeight || root.clientHeight)) /
100);
default:
return value;
}
}
/**
* File: /src/utilities/interfaces.ts *
* Author: Cameron Robinson *
* *
* Copyright (c) 2023 Cameron Robinson *
*/
class HTMLSizing {
get sizeValue() {
return this._sizeValue;
}
set sizeValue(value) {
this._sizeValue = value;
}
constructor(value, units) {
this._sizeValue = 0;
this.widthSet = false;
this.sizeUnits = "px";
this.sizeValue = value;
this.sizeUnits = units;
}
setWidth(value) {
this.widthSet = true;
return new HTMLSizing(value, this.sizeUnits);
}
setUnits(units) {
return new HTMLSizing(this.sizeValue, units);
}
toString() {
return `${this.sizeValue}${this.sizeUnits}`;
}
convertToPX(parentElement) {
if (this.sizeUnits === "px") {
return this;
}
switch (this.sizeUnits) {
case "cm":
case "mm":
case "in":
case "pt":
case "pc":
let absUnitsResult = pixels(`${this.toString()}`);
return new HTMLSizing(absUnitsResult, "px");
case "vw":
case "vh":
return handleViewportSizing();
case "em":
return getFontSizeFromEl();
case "ch":
let ch = createEl("p", {
"attr": { "style": "width: 1ch;" }
});
return getSizeFromStyleWidth(ch);
case "ex":
let ex = createEl("p", {
"attr": { "style": "width: 1ex;" }
});
return getSizeFromStyleWidth(ex);
}
function getSizeFromStyleWidth(el) {
const DEFAULT_SIZE = 16;
if (parentElement === null ||
parentElement === undefined) {
return new HTMLSizing(this.sizeValue * DEFAULT_SIZE, "px");
}
parentElement.appendChild(el);
let exToPxResult = this.sizeValue * el.clientWidth;
if (el.clientWidth === 0) {
exToPxResult = this.sizeValue * DEFAULT_SIZE;
}
parentElement.removeChild(el);
return new HTMLSizing(exToPxResult, "px");
}
function getFontSizeFromEl() {
const DEFAULT_SIZE = 16;
let fontSize = DEFAULT_SIZE;
let emToPxResult = fontSize * this.sizeValue;
if (parentElement === null ||
parentElement === undefined) {
return new HTMLSizing(emToPxResult, "px");
}
let sizing = HTMLSizing.parseToSizing(parentElement.getCssPropertyValue("font-size"));
if (sizing !== null) {
fontSize = sizing.sizeValue;
emToPxResult = fontSize * this.sizeValue;
}
return new HTMLSizing(emToPxResult, "px");
}
function handleViewportSizing() {
let scale = this.sizeValue / 100;
if (parentElement === null || parentElement === undefined) {
console.warn("Found undefined root element. Using default client size. Result may not appear as intended.");
let defaultSizing = scale * 1200;
return new HTMLSizing(defaultSizing, "px");
}
let viewWidth = parentElement.clientWidth;
let viewHeight = parentElement.clientHeight;
switch (this.sizeUnits) {
case "vw":
let wToPxResult = scale * viewWidth;
return new HTMLSizing(wToPxResult, "px");
case "vh":
case "%":
let hToPxResult = scale * viewHeight;
return new HTMLSizing(hToPxResult, "px");
}
}
}
static create() {
return new HTMLSizing(0, "px");
}
static parseToSizing(sizingText) {
if (sizingText === "") {
return null;
}
let unitData = HTMLSizing.getLengthUnit(sizingText);
if (unitData.isValid === true) {
let units = unitData.unitStr;
let sizeText = sizingText.replace(units, "").trim();
let size = parseInt(sizeText);
if (isNaN(size)) {
return null;
}
return HTMLSizing.create().setWidth(size).setUnits(units);
}
return null;
}
static getLengthUnit(lengthStr) {
let lastChar = lengthStr.slice(lengthStr.length - 1);
let lastTwoChars = lengthStr.slice(lengthStr.length - 2);
let unitStr = "";
let isValid = false;
if (lastChar === "%") {
unitStr = lastChar;
isValid = true;
}
else if (lastTwoChars === "cm" ||
lastTwoChars === "mm" ||
lastTwoChars === "in" ||
lastTwoChars === "px" ||
lastTwoChars === "pt" ||
lastTwoChars === "pc" ||
lastTwoChars === "em" ||
lastTwoChars === "ex" ||
lastTwoChars === "ch" ||
lastTwoChars === "vw" ||
lastTwoChars === "vh") {
unitStr = lastTwoChars;
isValid = true;
}
return { isValid: isValid, unitStr: unitStr };
}
}
addEventListener("mousedown", () => {
});
addEventListener("mouseup", () => {
});
function defaultStartRegionData() {
return {
found: false,
startPosition: -1,
endPosition: -1,
matchLength: 0,
regionType: "CODEBLOCK"
};
}
/*
* Filename: multi-column-markdown/src/regionSettings.ts
* Created Date: Tuesday, February 1st 2022, 12:23:53 pm
* Author: Cameron Robinson
*
* Copyright (c) 2022 Cameron Robinson
*/
var BorderOption;
(function (BorderOption) {
BorderOption[BorderOption["enabled"] = 0] = "enabled";
BorderOption[BorderOption["on"] = 1] = "on";
BorderOption[BorderOption["true"] = 2] = "true";
BorderOption[BorderOption["disabled"] = 3] = "disabled";
BorderOption[BorderOption["off"] = 4] = "off";
BorderOption[BorderOption["false"] = 5] = "false";
})(BorderOption || (BorderOption = {}));
var ShadowOption;
(function (ShadowOption) {
ShadowOption[ShadowOption["enabled"] = 0] = "enabled";
ShadowOption[ShadowOption["on"] = 1] = "on";
ShadowOption[ShadowOption["true"] = 2] = "true";
ShadowOption[ShadowOption["disabled"] = 3] = "disabled";
ShadowOption[ShadowOption["off"] = 4] = "off";
ShadowOption[ShadowOption["false"] = 5] = "false";
})(ShadowOption || (ShadowOption = {}));
var TableAlignOption;
(function (TableAlignOption) {
TableAlignOption[TableAlignOption["enabled"] = 0] = "enabled";
TableAlignOption[TableAlignOption["on"] = 1] = "on";
TableAlignOption[TableAlignOption["true"] = 2] = "true";
TableAlignOption[TableAlignOption["disabled"] = 3] = "disabled";
TableAlignOption[TableAlignOption["off"] = 4] = "off";
TableAlignOption[TableAlignOption["false"] = 5] = "false";
})(TableAlignOption || (TableAlignOption = {}));
const ALL_LAYOUTS = [
"standard",
"left",
"first",
"center",
"middle",
"second",
"right",
"third",
"last"
];
function isColumnLayout(value) {
return ALL_LAYOUTS.includes(value.toLowerCase());
}
function validateColumnLayout(value) {
return value.toLowerCase();
}
var ContentOverflowType;
(function (ContentOverflowType) {
ContentOverflowType[ContentOverflowType["scroll"] = 0] = "scroll";
ContentOverflowType[ContentOverflowType["hidden"] = 1] = "hidden";
})(ContentOverflowType || (ContentOverflowType = {}));
var AlignmentType;
(function (AlignmentType) {
AlignmentType[AlignmentType["left"] = 0] = "left";
AlignmentType[AlignmentType["center"] = 1] = "center";
AlignmentType[AlignmentType["right"] = 2] = "right";
})(AlignmentType || (AlignmentType = {}));
var TableAlignment;
(function (TableAlignment) {
TableAlignment[TableAlignment["useSettingsDefault"] = 0] = "useSettingsDefault";
TableAlignment[TableAlignment["align"] = 1] = "align";
TableAlignment[TableAlignment["noAlign"] = 2] = "noAlign";
})(TableAlignment || (TableAlignment = {}));
function MCSettings_isEqual(settingsA, settingsB) {
if ((settingsA === null || settingsA === void 0 ? void 0 : settingsA.columnID) !== (settingsB === null || settingsB === void 0 ? void 0 : settingsB.columnID)) {
return false;
}
if ((settingsA === null || settingsA === void 0 ? void 0 : settingsA.numberOfColumns) !== (settingsB === null || settingsB === void 0 ? void 0 : settingsB.numberOfColumns)) {
return false;
}
if (JSON.stringify(settingsA === null || settingsA === void 0 ? void 0 : settingsA.drawBorder) !== JSON.stringify(settingsB === null || settingsB === void 0 ? void 0 : settingsB.drawBorder)) {
return false;
}
if ((settingsA === null || settingsA === void 0 ? void 0 : settingsA.drawShadow) !== (settingsB === null || settingsB === void 0 ? void 0 : settingsB.drawShadow)) {
return false;
}
if ((settingsA === null || settingsA === void 0 ? void 0 : settingsA.autoLayout) !== (settingsB === null || settingsB === void 0 ? void 0 : settingsB.autoLayout)) {
return false;
}
if (JSON.stringify(settingsA === null || settingsA === void 0 ? void 0 : settingsA.columnSize) !== JSON.stringify(settingsB === null || settingsB === void 0 ? void 0 : settingsB.columnSize)) {
return false;
}
if ((settingsA === null || settingsA === void 0 ? void 0 : settingsA.columnPosition) !== (settingsB === null || settingsB === void 0 ? void 0 : settingsB.columnPosition)) {
return false;
}
if (JSON.stringify(settingsA === null || settingsA === void 0 ? void 0 : settingsA.columnSpacing) !== JSON.stringify(settingsB === null || settingsB === void 0 ? void 0 : settingsB.columnSpacing)) {
return false;
}
if (JSON.stringify(settingsA === null || settingsA === void 0 ? void 0 : settingsA.contentOverflow) !== JSON.stringify(settingsB === null || settingsB === void 0 ? void 0 : settingsB.contentOverflow)) {
return false;
}
if (JSON.stringify(settingsA === null || settingsA === void 0 ? void 0 : settingsA.alignment) !== JSON.stringify(settingsB === null || settingsB === void 0 ? void 0 : settingsB.alignment)) {
return false;
}
if ((settingsA === null || settingsA === void 0 ? void 0 : settingsA.columnHeight) !== (settingsB === null || settingsB === void 0 ? void 0 : settingsB.columnHeight)) {
return false;
}
if ((settingsA === null || settingsA === void 0 ? void 0 : settingsA.fullDocReflow) !== (settingsB === null || settingsB === void 0 ? void 0 : settingsB.fullDocReflow)) {
return false;
}
if ((settingsA === null || settingsA === void 0 ? void 0 : settingsA.alignTablesToAlignment) !== (settingsB === null || settingsB === void 0 ? void 0 : settingsB.alignTablesToAlignment)) {
return false;
}
return true;
}
function getDefaultMultiColumnSettings() {
return {
columnID: "",
numberOfColumns: 2,
drawBorder: [true],
drawShadow: true,
autoLayout: false,
columnSize: "standard",
columnPosition: "standard",
columnSpacing: [""],
contentOverflow: [ContentOverflowType.scroll],
alignment: [AlignmentType.left],
columnHeight: null,
fullDocReflow: false,
alignTablesToAlignment: TableAlignment.useSettingsDefault
};
}
function shouldDrawColumnBorder(index, settings) {
if (settings.drawBorder.length === 0) {
return true;
}
return getIndexedClampedArrayValue(index, settings.drawBorder);
}
function columnOverflowState(index, settings) {
if (settings.contentOverflow.length === 0) {
return ContentOverflowType.scroll;
}
return getIndexedClampedArrayValue(index, settings.contentOverflow);
}
function columnAlignmentState(index, settings) {
if (settings.alignment.length === 0) {
return AlignmentType.left;
}
return getIndexedClampedArrayValue(index, settings.alignment);
}
function columnSpacingState(index, settings) {
if (settings.columnSpacing.length === 0) {
return "";
}
return `margin-inline: ${getIndexedClampedArrayValue(index, settings.columnSpacing)};`;
}
function getIndexedClampedArrayValue(index, arr) {
if (arr.length === 0) {
throw ("Error getting value from empty array.");
}
if (index < 0) {
return arr[0];
}
if (index < arr.length) {
return arr[index];
}
return arr.last();
}
/**
* File: /src/utilities/settingsParser.ts
* Created Date: Friday, June 3rd 2022, 8:16 pm
* Author: Cameron Robinson
*
* Copyright (c) 2022 Cameron Robinson
*/
/**
* Here we define all of the valid settings strings that the user can enter for each setting type.
* The strings are then mapped twice, first to a valid regex string that searches for the setting
* name, ignoring all extra spaces and letter case, and then maped to a RegEx object to be used
* when parsing.
*/
const COL_POSITION_OPTION_STRS = [
"column position",
"col position",
"column location",
"col location",
"single column location",
"single column position",
];
const COL_POSITION_REGEX_ARR = COL_POSITION_OPTION_STRS.map(convertStringToSettingsRegex).map((value) => {
return new RegExp(value, "i");
});
const COL_SIZE_OPTION_STRS = [
"column size",
"column width",
"col size",
"col width",
"single column size",
"single col size",
"single column width",
"single col width",
"largest column"
];
const COL_SIZE_OPTION_REGEX_ARR = COL_SIZE_OPTION_STRS.map(convertStringToSettingsRegex).map((value) => {
return new RegExp(value, "i");
});
const NUMBER_OF_COLUMNS_STRS = [
"number of columns",
"num of cols",
"col count",
"column count"
];
const NUMBER_OF_COLUMNS_REGEX_ARR = NUMBER_OF_COLUMNS_STRS.map(convertStringToSettingsRegex).map((value) => {
return new RegExp(value, "i");
});
const DRAW_BORDER_STRS = [
"border"
];
const DRAW_BORDER_REGEX_ARR = DRAW_BORDER_STRS.map(convertStringToSettingsRegex).map((value) => {
return new RegExp(value, "i");
});
const DRAW_SHADOW_STRS = [
"shadow"
];
const DRAW_SHADOW_REGEX_ARR = DRAW_SHADOW_STRS.map(convertStringToSettingsRegex).map((value) => {
return new RegExp(value, "i");
});
const AUTO_LAYOUT_SETTING_STRS = [
"auto layout",
"fluid div",
"fluid divs",
"fluid columns",
"fluid cols",
"fluid col"
];
const AUTO_LAYOUT_REGEX_ARR = AUTO_LAYOUT_SETTING_STRS.map(convertStringToSettingsRegex).map((value) => {
return new RegExp(value, "i");
});
const COLUMN_SPACING_REGEX_ARR = [
"column spacing",
"column gap",
"column sep"
].map((value) => {
return new RegExp(convertStringToSettingsRegex(value), "i");
});
const COLUMN_HEIGHT_REGEX_ARR = [
"column height",
"col height",
"column max height",
"col max height",
"max column height",
"max col height"
].map((value) => {
return new RegExp(convertStringToSettingsRegex(value), "i");
});
const CONTENT_OVERFLOW_REGEX_ARR = [
"overflow",
"content overflow"
].map((value) => {
return new RegExp(convertStringToSettingsRegex(value), "i");
});
const ALIGNMENT_REGEX_ARR = [
"alignment",
"content alignment",
"align",
"content align",
"align content",
"text align",
"align text",
"Text Alignment"
].map((value) => {
return new RegExp(convertStringToSettingsRegex(value), "i");
});
const TABLE_ALIGNMENT_REGEX_ARR = [
"align tables to text alignment"
].map((value) => {
return new RegExp(convertStringToSettingsRegex(value), "i");
});
/**
* This function searches the settings string through each regex option. If one of the regex
* values match, it returns the first group found by the regex. This is depended on proper
* regex formatting which is done by the convertStringToSettingsRegex function defined below.
*
* @param settingsString The value that may match one of the setting options.
* @param validSettingFormatRegEx The settings options through which to check all options. If one of these regex
* values match on the string we break from the loop returning the found value.
*
* @returns the user entered data if the setting is a match, or null if non of the options matched.
*/
function getSettingsDataFromKeys(settingsString, validSettingFormatRegEx) {
for (let i = 0; i < validSettingFormatRegEx.length; i++) {
let regexSearchData = validSettingFormatRegEx[i].exec(settingsString);
if (regexSearchData !== null) {
return regexSearchData[1].trim();
}
}
return null;
}
function parseSingleColumnSettings(settingsStr, originalSettings) {
originalSettings.columnSize = "medium";
let settingsLines = settingsStr.split("\n");
for (let i = 0; i < settingsLines.length; i++) {
let settingsLine = settingsLines[i];
let settingsData = getSettingsDataFromKeys(settingsLine, COL_POSITION_REGEX_ARR);
if (settingsData !== null) {
originalSettings.columnPosition = parseForSingleColumnLocation(settingsData);
}
settingsData = getSettingsDataFromKeys(settingsLine, COL_SIZE_OPTION_REGEX_ARR);
if (settingsData !== null) {
originalSettings.columnSize = parseForSingleColumnSize(settingsData);
}
}
return originalSettings;
}
function parseColumnSettings(settingsStr) {
let parsedSettings = getDefaultMultiColumnSettings();
let settingsLines = settingsStr.split("\n");
for (let i = 0; i < settingsLines.length; i++) {
let settingsLine = settingsLines[i];
checkSettingIsRegionID(settingsLine, parsedSettings);
checkSettingIsNumberOfColumns(settingsLine, parsedSettings);
checkSettingDefinesColumnSize(settingsLine, parsedSettings);
checkSettingIsDrawBorder(settingsLine, parsedSettings);
checkSettingIsDrawShadow(settingsLine, parsedSettings);
checkSettingIsAutoLayout(settingsLine, parsedSettings);
checkSettingIsColumnSpacing(settingsLine, parsedSettings);
checkSettingIsContentOverflow(settingsLine, parsedSettings);
checkSettingIsColumnAlignment(settingsLine, parsedSettings);
checkSettingIsColumnHeight(settingsLine, parsedSettings);
checkSettingIsTableAlignment(settingsLine, parsedSettings);
}
return parsedSettings;
}
function checkSettingIsNumberOfColumns(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, NUMBER_OF_COLUMNS_REGEX_ARR);
if (settingsData === null) {
return;
}
let settingValues = parseForMultiSettings(settingsData);
settingsData = settingValues[0];
let numOfCols = parseInt(settingsData);
if (Number.isNaN(numOfCols) === false) {
if (numOfCols >= 1) {
parsedSettings.numberOfColumns = numOfCols;
}
}
}
function checkSettingIsRegionID(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, CODEBLOCK_REGION_ID_REGEX_ARR);
if (settingsData === null) {
return;
}
parsedSettings.columnID = settingsData;
}
function checkSettingDefinesColumnSize(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, COL_SIZE_OPTION_REGEX_ARR);
if (settingsData === null) {
return;
}
let settingValues = parseForMultiSettings(settingsData);
if (settingValues.length === 1) {
// If there is only 1 item we attempt to parse out a layout type. If we get a valid item we
// return here.
if (isColumnLayout(settingValues[0])) {
parsedSettings.columnSize = validateColumnLayout(settingValues[0]);
return;
}
}
let widths = [];
for (let setting of settingValues) {
let parsed = HTMLSizing.parseToSizing(setting.trim());
if (parsed !== null) {
widths.push(parsed);
}
}
// If none are parsed properly to a width then we return a default.
if (widths.length === 0) {
console.warn("Error parsing column layout or width, defaulting to standard layout.");
parsedSettings.columnSize = "standard";
return;
}
// If we parsed some lengths and some did not parse properly, the user has either
// poorly defined their settings or is attempting to break us. Take the first valid option
// between the two arrays.
if (widths.length !== settingValues.length) {
for (let setting of settingValues) {
let unitData = HTMLSizing.getLengthUnit(setting);
if (unitData.isValid === true) {
parsedSettings.columnSize = widths;
return;
}
if (isColumnLayout(settingValues[0])) {
parsedSettings.columnSize = validateColumnLayout(settingValues[0]);
return;
}
}
}
parsedSettings.columnSize = widths;
}
function checkSettingIsDrawBorder(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, DRAW_BORDER_REGEX_ARR);
if (settingsData === null) {
return;
}
let borders = [];
let settingValues = parseForMultiSettings(settingsData);
for (let settingsData of settingValues) {
let borderState = true;
let isBorderDrawn = BorderOption[settingsData];
if (isBorderDrawn !== undefined) {
switch (isBorderDrawn) {
case (BorderOption.disabled):
case (BorderOption.off):
case (BorderOption.false):
borderState = false;
break;
}
}
borders.push(borderState);
}
parsedSettings.drawBorder = borders;
}
function checkSettingIsDrawShadow(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, DRAW_SHADOW_REGEX_ARR);
if (settingsData === null) {
return;
}
let settingValues = parseForMultiSettings(settingsData);
settingsData = settingValues[0];
let isShadowDrawn = ShadowOption[settingsData];
if (isShadowDrawn !== undefined) {
switch (isShadowDrawn) {
case (ShadowOption.disabled):
case (ShadowOption.off):
case (ShadowOption.false):
parsedSettings.drawShadow = false;
break;
}
}
}
function checkSettingIsAutoLayout(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, AUTO_LAYOUT_REGEX_ARR);
if (settingsData === null) {
return;
}
let settingValues = parseForMultiSettings(settingsData);
settingsData = settingValues[0];
if (settingsData === "false" ||
settingsData === "off") {
parsedSettings.autoLayout = false;
}
parsedSettings.autoLayout = true;
}
function checkSettingIsColumnSpacing(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, COLUMN_SPACING_REGEX_ARR);
if (settingsData === null) {
return;
}
let spacings = [];
let settingValues = parseForMultiSettings(settingsData);
for (let settingsData of settingValues) {
let parsed = HTMLSizing.parseToSizing(settingsData.trim());
let spacingStr = "";
if (parsed !== null) {
spacingStr = parsed.toString();
}
else {
let noUnitsNum = parseInt(settingsData.trim());
if (isNaN(noUnitsNum) === false) {
spacingStr = `${noUnitsNum}pt`;
}
}
spacings.push(spacingStr);
}
parsedSettings.columnSpacing = spacings;
}
function checkSettingIsContentOverflow(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, CONTENT_OVERFLOW_REGEX_ARR);
if (settingsData === null) {
return;
}
let overflowStates = [];
let settingValues = parseForMultiSettings(settingsData);
for (let settingsData of settingValues) {
let overflowType = ContentOverflowType.scroll;
settingsData = settingsData.toLowerCase().trim();
if (settingsData === "hidden") {
overflowType = ContentOverflowType.hidden;
}
overflowStates.push(overflowType);
}
parsedSettings.contentOverflow = overflowStates;
}
function checkSettingIsColumnAlignment(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, ALIGNMENT_REGEX_ARR);
if (settingsData === null) {
return;
}
let alignments = [];
let settingValues = parseForMultiSettings(settingsData);
for (let settingsData of settingValues) {
let alignmentType = AlignmentType.left;
settingsData = settingsData.toLowerCase().trim();
if (settingsData === "center") {
alignmentType = AlignmentType.center;
}
if (settingsData === "right") {
alignmentType = AlignmentType.right;
}
alignments.push(alignmentType);
}
parsedSettings.alignment = alignments;
}
function checkSettingIsTableAlignment(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, TABLE_ALIGNMENT_REGEX_ARR);
if (settingsData === null) {
return;
}
let settingValues = parseForMultiSettings(settingsData);
settingsData = settingValues[0];
let tableAlignment = TableAlignOption[settingsData];
if (tableAlignment !== undefined) {
switch (tableAlignment) {
case (TableAlignOption.disabled):
case (TableAlignOption.off):
case (TableAlignOption.false):
parsedSettings.alignTablesToAlignment = TableAlignment.noAlign;
break;
default:
parsedSettings.alignTablesToAlignment = TableAlignment.align;
}
}
}
function checkSettingIsColumnHeight(settingsLine, parsedSettings) {
let settingsData = getSettingsDataFromKeys(settingsLine, COLUMN_HEIGHT_REGEX_ARR);
if (settingsData === null) {
return;
}
let settingValues = parseForMultiSettings(settingsData);
settingsData = settingValues[0];
let parsed = HTMLSizing.parseToSizing(settingsData.trim());
if (parsed !== null) {
parsedSettings.columnHeight = parsed;
}
else {
let noUnitsNum = parseInt(settingsData.trim());
if (isNaN(noUnitsNum) === false) {
parsedSettings.columnHeight = HTMLSizing.create().setWidth(noUnitsNum).setUnits("pt");
}
}
}
function parseForMultiSettings(originalValue) {
// Parse off brackets. If no brackets we return original value to be parsed as sole value.
let result = /\[(.*)\]/.exec(originalValue);
if (result === null) {
return [originalValue];
}
let settingsList = result[1];
let settings = settingsList.split(",").map((val) => {
return val.trim();
});
return settings;
}
const CODEBLOCK_REGION_ID_REGEX_STRS = [
"id",
"region id"
];
const CODEBLOCK_REGION_ID_REGEX_ARR = CODEBLOCK_REGION_ID_REGEX_STRS.map(convertStringToSettingsRegex).map((value) => {
return new RegExp(value, "i");
});
function parseStartRegionCodeBlockID(settingsStr) {
let codeBlockRegionID = "";
let settingsLines = settingsStr.split("\n");
for (let i = 0; i < settingsLines.length; i++) {
let settingsLine = settingsLines[i];
let settingsData = getSettingsDataFromKeys(settingsLine, CODEBLOCK_REGION_ID_REGEX_ARR);
if (settingsData !== null) {
codeBlockRegionID = settingsData;
}
}
return codeBlockRegionID;
}
function parseForSingleColumnLocation(locationString) {
switch (locationString.toLowerCase().trim().replace(" ", "")) {
case "left":
case "leftside":
case "leftmargin":
case "leftalign":
case "leftaligned":
case "leftalignement":
case "first":
case "start":
case "beginning":
return "left";
case "middle":
case "middlealigned":
case "middlealignment":
case "center":
case "centeraligned":
case "centeralignment":
case "centered":
case "standard":
return "center";
case "right":
case "rightside":
case "rightmargin":
case "rightalign":
case "rightaligned":
case "rightalignment":
case "last":
case "end":
return "right";
}
return "center";
}
function parseForSingleColumnSize(sizeString) {
switch (sizeString = sizeString.toLowerCase().trim().replace(" ", "")) {
case "small":
case "sm":
return "small";
case "medium":
case "med":
return "medium";
case "large":
case "lg":
return "large";
case "full":
case "full size":
return "full";
}
return "medium";
}
function convertStringToSettingsRegex(originalString) {
originalString = originalString.replace(" ", "(?:[-_]| *|)");
let regexString = `(?:${originalString} *[:=] *)(.*)`;
return regexString;
}
const PANDOC_ENGLISH_NUMBER_OF_COLUMNS = [
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten"
];
function isPandocNumberOfColumns(value) {
return PANDOC_ENGLISH_NUMBER_OF_COLUMNS.includes(value);
}
function validatePandocNumberOfColumns(value) {
return value.toLowerCase();
}
function pandocNumberOfColumnsToValue(value) {
switch (value) {
case "two":
return 2;
case "three":
return 3;
case "four":
return 4;
case "five":
return 5;
case "six":
return 6;
case "seven":
return 7;
case "eight":
return 8;
case "nine":
return 9;
case "ten":
return 10;
}
}
const PANDOC_COL_DOT_COUNT_NAME = "colDotCount";
const PANDOC_COL_NODOT_COUNT_NAME = "colCount";
const PANDOC_COl_SETTINGS = "colSettings";
const PANDOC_REGEX_STR = (() => {
let nums = PANDOC_ENGLISH_NUMBER_OF_COLUMNS.join("|");
let regex_strings = `:{3,} *(?:\\{ *\\.(?<${PANDOC_COL_DOT_COUNT_NAME}>(?:${nums}|))(?:[-_]|)columns(?<${PANDOC_COl_SETTINGS}>.*)\\}|(?<${PANDOC_COL_NODOT_COUNT_NAME}>(?:${nums}|))(?:[-_]|)columns)(?:[ :]*)$\\n?`;
return regex_strings;
})();
const PANDOC_REGEX = new RegExp(PANDOC_REGEX_STR, "m");
const PANDOC_OPEN_FENCE_REGEX = /^:{3,} *(?:[a-zA-Z]+|\{.*\})(?:[ :]*)$/m;
const PANDOC_CLOSE_FENCE_REGEX = /^:{3,} *$/m;
function findPandoc(text) {
let regexData = PANDOC_REGEX.exec(text);
if (regexData !== null) {
let data = defaultPandocRegexData();
data.found = true;
data.startPosition = regexData.index;
data.endPosition = regexData.index + regexData[0].length;
let regionData = reducePandocRegionToEndDiv(text.slice(data.endPosition));
data.endPosition += regionData.content.length + regionData.matchLength;
data.content = regionData.content;
data.matchLength = data.endPosition - data.startPosition;
data.userSettings = regexData.groups[PANDOC_COl_SETTINGS] ? regexData.groups[PANDOC_COl_SETTINGS] : "";
data.columnCount = regexData.groups[PANDOC_COL_DOT_COUNT_NAME] ? regexData.groups[PANDOC_COL_DOT_COUNT_NAME] : regexData.groups[PANDOC_COL_NODOT_COUNT_NAME];
return data;
}
return defaultPandocRegexData();
}
function getPandocStartData(text) {
let data = findPandoc(text);
if (data.found === false) {
return {
found: false,
userSettings: getDefaultMultiColumnSettings()
};
}
return {
found: true,
userSettings: parsePandocSettings(data.userSettings, data.columnCount)
};
}
function containsPandoc(text) {
return findPandoc(text).found;
}
function containsPandocStartTag(text) {
let regexData = PANDOC_REGEX.exec(text);
if (regexData !== null) {
return true;
}
return false;
}
function containsPandocEndTag(text) {
let regexData = PANDOC_CLOSE_FENCE_REGEX.exec(text);
if (regexData !== null) {
return true;
}
return false;
}
function isValidPandocEndTag(linesAbove, currentLine) {
if (containsPandocEndTag(currentLine) === false) {
return false;
}
let contentText = linesAbove.concat(currentLine).join("\n");
return reducePandocRegionToEndDiv(contentText).found;
}
function reducePandocRegionToEndDiv(contentText) {
let workingText = contentText;
let result = {
found: false,
content: workingText,
matchLength: 0
};
let state = 0;
let offset = 0;
for (let i = 0; true; i++) {
if (i > 100) {
break;
}
let fence = getNextPandocFence(workingText);
if (fence === null) {
break;
}
let result = fence.result;
if (fence.type === "close") {
// console.log(workingText.slice(result.index, result.index + result[0].length));
offset += (result.index + result[0].length);
state--;
}
else {
// console.log(workingText.slice(result.index, result.index + result[0].length));
offset += (result.index + result[0].length);
state++;
}
if (state === -1) {
// We have found our last close tag.
return buildReturnData(result);
}
workingText = contentText.slice(offset);
}
function buildReturnData(matchResult) {
result.content = contentText.slice(0, offset - matchResult[0].length);
result.matchLength = matchResult[0].length;
result.found = true;
return result;
}
return result;
}
function getNextPandocFence(workingText) {
let openResult = PANDOC_OPEN_FENCE_REGEX.exec(workingText);
let closeResult = PANDOC_CLOSE_FENCE_REGEX.exec(workingText);
if (openResult === null && closeResult === null) {
return null;
}
if (openResult === null && closeResult !== null) {
return {
result: closeResult,
type: "close"
};
}
if (closeResult === null && openResult !== null) {
return {
result: openResult,
type: "open"
};
}
if (closeResult.index < openResult.index) {
return {
result: closeResult,
type: "close"
};
}
else {
return {
result: openResult,
type: "open"
};
}
}
function findPandocStart(text) {
let startRegion = defaultStartRegionData();
startRegion.regionType = "PADOC";
let regexData = PANDOC_REGEX.exec(text);
if (regexData !== null && regexData.length > 0) {
startRegion.found = true;
startRegion.startPosition = regexData.index;
startRegion.matchLength = regexData[0].length;
startRegion.endPosition = startRegion.startPosition + startRegion.matchLength;
}
return startRegion;
}
function defaultPandocRegexData() {
return {
found: false,
startPosition: -1,
endPosition: -1,
matchLength: 0,
content: "",
userSettings: "",
columnCount: "",
regionType: "PADOC"
};
}
const PANDOC_SETTING_REGEX = /(?<settingName>[^ ]*)=(?<settingValue>".*"|[^ =]*)/;
function parsePandocSettings(pandocUserSettings, colCount = "") {
//TODO: Add option for column rule.
let defaultSettings = getDefaultMultiColumnSettings();
let colCountDefined = false;
if (colCount !== "" && isPandocNumberOfColumns(colCount)) {
colCountDefined = true;
defaultSettings.numberOfColumns = pandocNumberOfColumnsToValue(validatePandocNumberOfColumns(colCount));
}
if (pandocUserSettings.replace(" ", "") === "") {
return defaultSettings;
}
let workingString = pandocUserSettings;
let regexValue = PANDOC_SETTING_REGEX.exec(workingString);
let settingList = "";
for (let i = 0; regexValue !== null; i < 100) {
let settingName = regexValue.groups['settingName'];
let settingValue = regexValue.groups['settingValue'];
settingList += `${settingName}: ${settingValue}\n`;
workingString = workingString.slice(regexValue.index + regexValue[0].length);
regexValue = PANDOC_SETTING_REGEX.exec(workingString);
}
let parsedSettings = parseColumnSettings(settingList);
if (colCountDefined) {
parsedSettings.numberOfColumns = defaultSettings.numberOfColumns;
}
return parsedSettings;
}
/*
* File: multi-column-markdown/src/MultiColumnParser.ts
* Created Date: Saturday, January 22nd 2022, 6:02:46 pm
* Author: Cameron Robinson
*
* Copyright (c) 2022 Cameron Robinson
*/
const START_REGEX_STRS = ["(===|---) *start-multi-column(:?[a-zA-Z0-9-_\\s]*)?",
"(===|---) *multi-column-start(:?[a-zA-Z0-9-_\\s]*)?"];
const START_REGEX_ARR = [];
for (let i = 0; i < START_REGEX_STRS.length; i++) {
START_REGEX_ARR.push(new RegExp(START_REGEX_STRS[i]));
}
const START_REGEX_STRS_WHOLE_LINE = ["^(===|---) *start-multi-column(:?[a-zA-Z0-9-_\\s]*)?$",
"^(===|---) *multi-column-start(:?[a-zA-Z0-9-_\\s]*)?$"];
const START_REGEX_ARR_WHOLE_LINE = [];
for (let i = 0; i < START_REGEX_STRS_WHOLE_LINE.length; i++) {
START_REGEX_ARR_WHOLE_LINE.push(new RegExp(START_REGEX_STRS_WHOLE_LINE[i]));
}
function findStartTag(text) {
let startRegion = defaultStartRegionData();
startRegion.regionType = "ORIGINAL";
for (let i = 0; i < START_REGEX_ARR.length; i++) {
let regexData = START_REGEX_ARR[i].exec(text);
if (regexData !== null && regexData.length > 0) {
startRegion.startPosition = regexData.index;
startRegion.matchLength = regexData[0].length;
startRegion.endPosition = startRegion.startPosition + startRegion.matchLength;
let line = text.slice(startRegion.startPosition, startRegion.endPosition);
if (START_REGEX_ARR_WHOLE_LINE[i].test(line)) {
startRegion.found = true;
break;
}
}
}
return startRegion;
}
function containsStartTag(text) {
return findStartTag(text).found;
}
function isStartTagWithID(text) {
let startTagData = findStartTag(text);
if (startTagData.found === true) {
let key = getStartTagKey(text);
if (key === null || key === "") {
return { isStartTag: true, hasKey: false };
}
return { isStartTag: true, hasKey: true };
}
return { isStartTag: false, hasKey: false };
}
const END_REGEX_STRS = ["--- *end-multi-column",
"--- *multi-column-end",
"=== *end-multi-column",
"=== *multi-column-end"];
const END_REGEX_ARR = [];
for (let i = 0; i < END_REGEX_STRS.length; i++) {
END_REGEX_ARR.push(new RegExp(END_REGEX_STRS[i]));
}
function findEndTag(text) {
// We want to find the first end tag in the text.
// So here we loop backwards, slicing off the tail until
// there are no more end tags available
let lastValidData = getEndTagData(text);
let workingRegexData = lastValidData;
while (workingRegexData.found === true) {
lastValidData = workingRegexData;
text = text.slice(0, workingRegexData.startPosition);
workingRegexData = getEndTagData(text);
}
return lastValidData;
}
function findEndTagClosestToEnd(text) {
let workingText = text;
let offset = 0;
let lastValidData = getEndTagData(workingText);
while (lastValidData.found) {
workingText = workingText.slice(lastValidData.endPosition);
let newData = getEndTagData(workingText);
if (newData.found === false) {
break;
}
offset += lastValidData.endPosition;
lastValidData = newData;
}
return {
found: lastValidData.found,
startPosition: lastValidData.startPosition + offset,
endPosition: lastValidData.endPosition + offset,
matchLength: lastValidData.matchLength
};
}
function containsEndTag(text) {
return findEndTag(text).found;
}
function getEndTagData(text) {
let found = false;
let startPosition = -1;
let endPosition = -1;
let matchLength = 0;
for (let i = 0; i < END_REGEX_ARR.length; i++) {
let regexData = END_REGEX_ARR[i].exec(text);
if (regexData !== null && regexData.length > 0) {
found = true;
startPosition = regexData.index;
matchLength = regexData[0].length;
break;
}
}
endPosition = startPosition + matchLength;
return { found, startPosition, endPosition, matchLength };
}
const COL_REGEX_STRS = [["^===\\s*?column-end\\s*?===\\s*?$", ""],
["^===\\s*?end-column\\s*?===\\s*?$", ""],
["^===\\s*?column-break\\s*?===\\s*?$", ""],
["^===\\s*?break-column\\s*?===\\s*?$", ""],
["^---\\s*?column-end\\s*?---\\s*?$", ""],
["^---\\s*?end-column\\s*?---\\s*?$", ""],
["^---\\s*?column-break\\s*?---\\s*?$", ""],
["^---\\s*?break-column\\s*?---\\s*?$", ""],
["^ *?(?:\\?)\\columnbreak *?$", ""],
["^:{3,} *column-?break *(?:(?:$\\n^)?| *):{3,} *$", "m"]];
const COL_REGEX_ARR = [];
for (let i = 0; i < COL_REGEX_STRS.length; i++) {
COL_REGEX_ARR.push(new RegExp(COL_REGEX_STRS[i][0], COL_REGEX_STRS[i][1]));
}
function containsColEndTag(text) {
let found = false;
for (let i = 0; i < COL_REGEX_ARR.length; i++) {
if (COL_REGEX_ARR[i].test(text)) {
found = true;
break;
}
}
return found;
}
const INNER_COL_END_REGEX_ARR = [
/^-{3}\s*?column-end\s*?-{3}\s*?$\n?/m,
/^-{3}\s*?end-column\s*?-{3}\s*?$\n?/m,
/^-{3}\s*?column-break\s*?-{3}\s*?$\n?/m,
/^-{3}\s*?break-column\s*?-{3}\s*?$\n?/m,
/^={3}\s*?column-end\s*?={3}\s*?$\n?/m,
/^={3}\s*?end-column\s*?={3}\s*?$\n?/m,
/^={3}\s*?column-break\s*?={3}\s*?$\n?/m,
/^={3}\s*?break-column\s*?={3}\s*?$\n?/m,
/^ *?(?:\\?)\\columnbreak *?$\n?/m,
/^:{3,} *column-?break *(?:(?:$\n^)?| *):{3,} *$/m
];
function checkForParagraphInnerColEndTag(text) {
for (let i = 0; i < INNER_COL_END_REGEX_ARR.length; i++) {
let regexResult = INNER_COL_END_REGEX_ARR[i].exec(text);
if (regexResult) {
return regexResult;
}
}
return null;
}
const COL_ELEMENT_INNER_TEXT_REGEX_STRS = ["= *column-end *=",
"= *end-column *=",
"= *column-break *=",
"= *break-column *="];
const COL_ELEMENT_INNER_TEXT_REGEX_ARR = [];
for (let i = 0; i < COL_ELEMENT_INNER_TEXT_REGEX_STRS.length; i++) {
COL_ELEMENT_INNER_TEXT_REGEX_ARR.push(new RegExp(COL_ELEMENT_INNER_TEXT_REGEX_STRS[i]));
}
function elInnerTextContainsColEndTag(text) {
let found = false;
for (let i = 0; i < COL_ELEMENT_INNER_TEXT_REGEX_ARR.length; i++) {
if (COL_ELEMENT_INNER_TEXT_REGEX_ARR[i].test(text)) {
found = true;
break;
}
}
return found;
}
const COL_SETTINGS_REGEX_STRS = ["```settings",
"```column-settings",
"```multi-column-settings"];
const COL_SETTINGS_REGEX_ARR = [];
for (let i = 0; i < COL_SETTINGS_REGEX_STRS.length; i++) {
COL_SETTINGS_REGEX_ARR.push(new RegExp(COL_SETTINGS_REGEX_STRS[i]));
}
function containsColSettingsTag(text) {
let found = false;
for (let i = 0; i < COL_SETTINGS_REGEX_ARR.length; i++) {
if (COL_SETTINGS_REGEX_ARR[i].test(text)) {
found = true;
break;
}
}
return found;
}
function findSettingsCodeblock(text) {
let found = false;
let startPosition = -1;
let endPosition = -1;
let matchLength = 0;
for (let i = 0; i < COL_SETTINGS_REGEX_ARR.length; i++) {
let regexData = COL_SETTINGS_REGEX_ARR[i].exec(text);
if (regexData !== null && regexData.length > 0) {
found = true;
startPosition = regexData.index;
matchLength = regexData[0].length;
endPosition = startPosition + matchLength;
let remainingText = text.slice(endPosition);
regexData = CODEBLOCK_END_REGEX.exec(remainingText);
if (regexData !== null && regexData.length > 0) {
found = true;
endPosition += regexData.index + regexData[0].length;
}
break;
}
}
return {
found,
startPosition,
endPosition,
matchLength,
regionType: "CODEBLOCK"
};
}
const CODEBLOCK_START_REGEX_STR = [
"multi-column-start",
"start-multi-column"
].reduce((prev, cur) => {
if (prev === "") {
return cur;
}
return `${prev}|${cur}`;
}, "");
const START_CODEBLOCK_REGEX = new RegExp(`\`\`\`(:?${CODEBLOCK_START_REGEX_STR})(.*?)\`\`\``, "ms");
function findStartCodeblock(text) {
let startRegion = defaultStartRegionData();
startRegion.regionType = "CODEBLOCK";
let regexData = START_CODEBLOCK_REGEX.exec(text);
if (regexData !== null && regexData.length > 0) {
startRegion.found = true;
startRegion.startPosition = regexData.index;
startRegion.matchLength = regexData[0].length;
startRegion.endPosition = startRegion.startPosition + startRegion.matchLength;
}
return startRegion;
}
function containsStartCodeBlock(text) {
return findStartCodeblock(text).found;
}
function containsRegionStart(text) {
return containsStartCodeBlock(text) || containsStartTag(text) || containsPandoc(text);
}
function countStartTags(initialText) {
let keys = [];
let text = initialText;
let startTagData = findStartTag(text);
while (startTagData.found) {
// Slice off everything before the tag
text = text.slice(startTagData.startPosition);
/**
* Get just the start tag line and then set text to everything just
* after the start tag.
*/
let tag = text.split("\n")[0];
text = text.slice(1); // This moves the text 1 character so we dont match the same tag.
// Parse out the key and append to the list.
let key = getStartTagKey(tag);
if (key === null) {
key = "";
}
keys.push(key);
// Search again for another tag before looping.
startTagData = findStartTag(text);
}
text = initialText;
startTagData = findStartCodeblock(text);
while (startTagData.found) {
let settingsText = text.slice(startTagData.startPosition, startTagData.endPosition);
text = text.slice(startTagData.endPosition);
let key = parseStartRegionCodeBlockID(settingsText);
if (key === null) {
key = "";
}
keys.push(key);
// Search again for another tag before looping.
startTagData = findStartCodeblock(text);
}
return { numberOfTags: keys.length, keys };
}
function getStartDataAboveLine(linesAboveArray) {
return getStartBlockOrCodeblockAboveLine(linesAboveArray, [
findStartTag,
findStartCodeblock,
findPandocStart
]);
}
function getStartBlockOrCodeblockAboveLine(linesAboveArray, searchFunctions) {
let originalText = linesAboveArray.join("\n");
let { tagMatchData, lastFoundTag, textAbove } = findLastValidTag(originalText);
if (tagMatchData === null) {
return null;
}
if (tagMatchData.found === false) {
return null;
}
if (tagMatchData.regionType === "CODEBLOCK") {
let endTagSerachData = findEndTag(textAbove);
if (endTagSerachData.found === true) {
return null;
}
let startBlockKey = parseStartRegionCodeBlockID(lastFoundTag);
let linesAboveArray = textAbove.split("\n");
return { startBlockKey, linesAboveArray, startBlockType: "CODEBLOCK" };
}
if (tagMatchData.regionType === "ORIGINAL") {
let endTagSerachData = findEndTag(textAbove);
if (endTagSerachData.found === true) {
return null;
}
let linesAboveArray = textAbove.split("\n");
let startBlockKey = getStartTagKey(lastFoundTag);
let codeBlockData = parseCodeBlockStart(linesAboveArray);
if (codeBlockData !== null) {
startBlockKey = codeBlockData.id;
if (codeBlockData.index > 0) {
linesAboveArray = linesAboveArray.slice(codeBlockData.index + 1);
}
}
if (startBlockKey === null) {
startBlockKey = "";
}
return { startBlockKey, linesAboveArray, startBlockType: "ORIGINAL" };
}
if (tagMatchData.regionType === "PADOC") {
let endTagSerachData = reducePandocRegionToEndDiv(textAbove);
if (endTagSerachData.found === true) {
return null;
}
let linesAboveArray = textAbove.split("\n");
let pandocData = getPandocStartData(`${lastFoundTag}`);
let startBlockKey = pandocData.userSettings.columnID;
return {
startBlockKey,
linesAboveArray,
startBlockType: "PADOC"
};
}
return null;
function findLastValidTag(originalText) {
let textAbove = originalText;
let offset = 0;
let tagMatchData = null;
let lastFoundTag = "";
for (let i = 0; true; i++) {
if (i > 100) {
break;
}
let tagsFound = [];
searchFunctions.forEach((func) => {
tagsFound.push(func(textAbove));
});
tagsFound = tagsFound.filter((val) => {
return val.found === true;
}).sort((a, b) => {
return a.startPosition - b.startPosition;
});
if (tagsFound.length === 0) {
break;
}
tagMatchData = tagsFound[0];
let startIndex = offset + tagMatchData.startPosition;
lastFoundTag = originalText.slice(startIndex, startIndex + tagMatchData.matchLength).trimEnd();
offset += (tagMatchData.startPosition + tagMatchData.matchLength);
textAbove = originalText.slice(offset);
}
return {
tagMatchData,
lastFoundTag,
textAbove
};
}
}
/**
* This function will filter a set of strings, returning all items starting
* from the closest open start tag through the last item in the set.
*
* The function filters out all end tags to make sure that the start tag we
* find is the proper start tag for the list sent.
* @param linesAboveArray
* @returns
*/
function getStartBlockAboveLine(linesAboveArray) {
return getStartBlockOrCodeblockAboveLine(linesAboveArray, [findStartTag]);
}
function getEndBlockBelow(linesBelow) {
// Reduce the array down into a single string so that we can
// easily RegEx over the string and find the indicies we're looking for.
let linesBelowStr = linesBelow.reduce((prev, current) => {
return prev + "\n" + current;
}, "");
let endTagSerachData = findEndTag(linesBelowStr);
let startTagSearchData = findStartTag(linesBelowStr);
let sliceEndIndex = -1; // If neither start or end found we return the entire array.
if (endTagSerachData.found === true && startTagSearchData.found === false) {
sliceEndIndex = endTagSerachData.startPosition;
}
else if (endTagSerachData.found === false && startTagSearchData.found === true) {
sliceEndIndex = startTagSearchData.startPosition;
}
else if (endTagSerachData.found === true && startTagSearchData.found === true) {
sliceEndIndex = endTagSerachData.startPosition;
if (startTagSearchData.startPosition < endTagSerachData.startPosition) {
/**
* If we found a start tag before an end tag we want to use the start tag
* our current block is not properly ended and we use the next start tag
* as our limit
*/
sliceEndIndex = startTagSearchData.startPosition;
}
}
return linesBelow.slice(0, sliceEndIndex);
}
function getStartTagKey(startTag) {
let keySplit = startTag.split(":");
if (keySplit.length > 1) {
return keySplit[1].replace(" ", "");
}
return null;
}
const TAB_HEADER_END_REGEX_STR = "^```$";
const TAB_HEADER_END_REGEX = new RegExp(TAB_HEADER_END_REGEX_STR);
function parseCodeBlockStart(codeBlockLines) {
let id = null;
for (let i = 0; i < codeBlockLines.length; i++) {
let line = codeBlockLines[i];
if (id === null) {
let key = line.split(":")[0];
if (key.toLowerCase() === "region id") {
id = line.split(":")[1].trim();
}
}
else {
if (TAB_HEADER_END_REGEX.test(line)) {
return { id: id, index: i };
}
}
}
if (id === null) {
return null;
}
else {
return { id: id, index: -1 };
}
}
const CODEBLOCK_END_REGEX_STR = "```";
const CODEBLOCK_END_REGEX = new RegExp(CODEBLOCK_END_REGEX_STR);
/*
* Filename: multi-column-markdown/src/utilities/utils.ts
* Created Date: Tuesday, January 30th 2022, 4:02:19 pm
* Author: Cameron Robinson
*
* Copyright (c) 2022 Cameron Robinson
*/
function getUID(length = 10) {
if (length > 10) {
length = 10;
}
let UID = Math.random().toString(36).substring(2);
UID = UID.slice(0, length);
return UID;
}
/**
* BFS on the child nodes of the passed element searching for the first instance of the
* node type passed. Returning the element found or null if none found.
*
* @param root
* @param nodeTypeName
* @returns
*/
function searchChildrenForNodeType(root, nodeTypeName) {
nodeTypeName = nodeTypeName.toLowerCase();
let queue = [root];
while (queue.length > 0) {
for (let i = 0; i < queue.length; i++) {
let node = queue.shift();
let nodeName = node.nodeName.toLowerCase();
if (nodeName === nodeTypeName) {
return node;
}
for (let i = 0; i < node.children.length; i++) {
queue.push(node.children[i]);
}
}
}
return null;
}
function fileStillInView(sourcePath) {
let fileLeaf = getFileLeaf(sourcePath);
if (fileLeaf === null) {
return false;
}
return true;
}
function getFileLeaf(sourcePath) {
let markdownLeaves = app.workspace.getLeavesOfType("markdown");
if (markdownLeaves.length === 0) {
return null;
}
for (let i = 0; i < markdownLeaves.length; i++) {
if (markdownLeaves[i].getViewState().state.file === sourcePath) {
return markdownLeaves[i];
}
}
return null;
}
/*
* Filename: multi-column-markdown/src/domObject.ts
* Created Date: Tuesday, February 1st 2022, 12:04:00 pm
* Author: Cameron Robinson
*
* Copyright (c) 2022 Cameron Robinson
*/
const UPDATE_TIMES = [125, 125, 250, 20000];
const MID_BREAK_ERROR_MESSAGE = "Detected invalid column break syntax.\nPlease make sure column break tags are not in the middle of a paragraph block";
var DOMObjectTag;
(function (DOMObjectTag) {
DOMObjectTag[DOMObjectTag["none"] = 0] = "none";
DOMObjectTag[DOMObjectTag["startRegion"] = 1] = "startRegion";
DOMObjectTag[DOMObjectTag["regionSettings"] = 2] = "regionSettings";
DOMObjectTag[DOMObjectTag["columnBreak"] = 3] = "columnBreak";
DOMObjectTag[DOMObjectTag["endRegion"] = 4] = "endRegion";
})(DOMObjectTag || (DOMObjectTag = {}));
var ElementColumnBreakType;
(function (ElementColumnBreakType) {
ElementColumnBreakType[ElementColumnBreakType["none"] = 0] = "none";
ElementColumnBreakType[ElementColumnBreakType["preBreak"] = 1] = "preBreak";
ElementColumnBreakType[ElementColumnBreakType["postBreak"] = 2] = "postBreak";
ElementColumnBreakType[ElementColumnBreakType["midBreak"] = 3] = "midBreak";
})(ElementColumnBreakType || (ElementColumnBreakType = {}));
class DOMObject {
constructor(element, linesOfElement, randomID = getUID(), tag = DOMObjectTag.none) {
this.clonedElement = null;
this.elementIsColumnBreak = ElementColumnBreakType.none;
this.elementType = "undefined";
this.elementContainer = null;
this.elementRenderedHeight = 0;
this.canvasElementUpdateTime = Date.now();
this.canvasTimerIndex = 0;
this.lastClonedElementUpdateTime = Date.now();
this.updateTimerIndex = 0;
this.nodeKey = element.innerText.trim();
this.originalElement = element;
this.UID = randomID;
this.tag = tag;
this.usingOriginalElement = false;
this.linesOfElement = linesOfElement;
if (this.tag === DOMObjectTag.none) {
this.setDomObjectTag();
}
// If our tag is still none here, we now want to check for
// an in paragraph column break flag.
if (this.tag === DOMObjectTag.none) {
this.checkForPrePostColumnBreak();
}
}
setMainDOMElement(domElement) {
this.originalElement = domElement;
this.usingOriginalElement = true;
}
clonedElementReadyForUpdate() {
let deltaTime = Date.now() - this.lastClonedElementUpdateTime;
if (deltaTime > UPDATE_TIMES[this.updateTimerIndex]) {
return true;
}
return false;
}
canvasReadyForUpdate() {
var timingArray = UPDATE_TIMES;
let { requiresUpdate, timerIndex, updateTime } = checkIfTimingIsReadyForUpdate(timingArray, this.canvasElementUpdateTime, this.canvasTimerIndex);
if (requiresUpdate === false) {
return false;
}
this.canvasElementUpdateTime = updateTime;
this.canvasTimerIndex = timerIndex;
return true;
}
updateClonedElement(newClonedElement) {
this.clonedElement = newClonedElement;
this.updateClonedElementTimer();
}
updateClonedElementTimer() {
this.lastClonedElementUpdateTime = Date.now();
this.updateTimerIndex = Math.clamp(this.updateTimerIndex + 1, 0, UPDATE_TIMES.length - 1);
}
setDomObjectTag() {
let elementTextSpaced = this.linesOfElement.reduce((prev, curr) => {
return prev + "\n" + curr;
});
if (containsEndTag(this.originalElement.textContent) === true) {
this.elementType = "unRendered";
this.tag = DOMObjectTag.endRegion;
// el.addClass(MultiColumnStyleCSS.RegionEndTag)
// regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.endRegion);
}
else if (containsColEndTag(this.originalElement.textContent) === true ||
(this.originalElement.innerHTML.startsWith("<mark>")) && elInnerTextContainsColEndTag(this.originalElement.textContent)) {
this.elementType = "unRendered";
this.tag = DOMObjectTag.columnBreak;
// el.addClass(MultiColumnStyleCSS.ColumnEndTag)
// regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.columnBreak);
}
else if (containsStartTag(this.originalElement.textContent) === true) {
this.elementType = "unRendered";
this.tag = DOMObjectTag.startRegion;
// el.addClass(MultiColumnStyleCSS.ColumnEndTag)
// regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.columnBreak);
}
else if (containsColSettingsTag(elementTextSpaced) === true) {
this.elementType = "unRendered";
// el.addClass(MultiColumnStyleCSS.RegionSettings)
// regionalManager = regionalContainer.setRegionSettings(elementTextSpaced)
// regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.regionSettings);
}
}
checkForPrePostColumnBreak() {
function replaceColBreak(text) {
let colBreakData = checkForParagraphInnerColEndTag(text);
if (containsColumnBreak === null) {
return text;
}
let startIndex = colBreakData.index;
let endIndex = startIndex + colBreakData[0].length;
let pre = text.slice(0, startIndex);
let post = text.slice(endIndex);
return `${pre}${post}`;
}
let textOfElement = this.originalElement.innerText;
let containsColumnBreak = checkForParagraphInnerColEndTag(textOfElement);
if (containsColumnBreak !== null) {
let text = this.originalElement.innerText;
let startIndex = containsColumnBreak.index;
let endIndex = startIndex + containsColumnBreak[0].length;
let pre = text.slice(0, startIndex);
let post = text.slice(endIndex);
// Sometimes the element passed in is a DIV containing a child element, other
// times it is the root child element alone, here we just make sure we are accessing
// the right element we want.
let checkNode = this.originalElement;
if (this.originalElement.nodeName === "DIV") {
checkNode = this.originalElement.children[0];
}
let paragraph = null;
if (checkNode.nodeName === "P") {
// Paragraphs simply remove the col-break tag
// we set our element here incase we need to display an error.
paragraph = checkNode;
checkNode.innerText = `${pre}${post}`;
}
else if (checkNode.nodeName === "UL" || checkNode.nodeName === "OL") {
// Attempt to get the list item that contains the column break,
// From testing this code should only run when the column break is at the end
// of a list not at the start of the list.
let listItem = null;
for (let i = checkNode.children.length - 1; i >= 0; i--) {
if (checkNode.children[i].nodeName === "LI") {
listItem = checkNode.children[i];
break;
}
}
if (listItem !== null) {
// Replace, the list element HTML without the col-break text.
listItem.innerHTML = replaceColBreak(listItem.innerHTML);
}
}
else {
console.debug(`Element Type: ${checkNode.nodeName}, does not currently support appened column-breaks.`, checkNode.cloneNode(true));
// if(paragraph) {
// paragraph.innerText = `${pre}${post}`;
// }
}
// console.debug("Checking where column break is", startIndex, endIndex, text.length);
if (startIndex === 0) {
// console.debug("Column break at begining of element.")
this.elementIsColumnBreak = ElementColumnBreakType.preBreak;
}
else if (endIndex === text.length) {
// console.debug("Column break at end of element.")
this.elementIsColumnBreak = ElementColumnBreakType.postBreak;
}
else {
// console.debug("Column break in the middle of element?")
this.elementIsColumnBreak = ElementColumnBreakType.midBreak;
const ERROR_COLOR_CSS = "mcm-error-message-color";
const CENTER_ALIGN_SPAN_CSS = "mcm-span-content-alignment-center";
if (paragraph) {
paragraph.innerHTML = `${pre}\n<span class="${ERROR_COLOR_CSS} ${CENTER_ALIGN_SPAN_CSS}">${MID_BREAK_ERROR_MESSAGE}</span>\n\n${post}`.split("\n").join("<br>");
}
}
}
}
}
class TaskListDOMObject extends DOMObject {
constructor(baseDOMObject) {
super(baseDOMObject.originalElement, baseDOMObject.linesOfElement, baseDOMObject.UID, DOMObjectTag.none);
this.originalCheckboxes = [];
this.checkboxElements = new Map();
}
checkboxClicked(index) {
if (this.checkboxElements.has(index)) {
this.checkboxElements.get(index).click();
}
if (index < this.originalCheckboxes.length) {
let originalInput = this.originalCheckboxes[index].getElementsByClassName('task-list-item-checkbox');
if (originalInput.length === 1) {
originalInput[0].click();
}
// else {
// console.error("Could not find checkbox to click.")
// }
}
}
getCheckboxElement(index) {
var _a;
if (this.checkboxElements.has(index) === false) {
if (index < this.originalCheckboxes.length) {
let originalInput = (_a = this.originalCheckboxes[index]) === null || _a === void 0 ? void 0 : _a.getElementsByClassName('task-list-item-checkbox');
if ((originalInput === null || originalInput === void 0 ? void 0 : originalInput.length) >= 1) {
this.checkboxElements.set(index, originalInput[0]);
}
// else {
// console.error("Could not find checkbox element to return.", this.originalCheckboxes, index);
// }
}
}
return this.checkboxElements.get(index);
}
static checkForTaskListElement(domElement) {
if (domElement.originalElement.getElementsByClassName("task-list-item").length > 0) {
return new TaskListDOMObject(domElement);
}
return domElement;
}
static getChildCheckbox(el) {
let checkboxElements = el.getElementsByClassName('task-list-item-checkbox');
if (checkboxElements.length === 1) {
return checkboxElements[0];
}
return el.children[0];
}
}
function checkIfTimingIsReadyForUpdate(timingArray, canvasElementUpdateTime, canvasTimerIndex) {
let deltaTime = Date.now() - canvasElementUpdateTime;
if (deltaTime > timingArray[canvasTimerIndex]) {
canvasElementUpdateTime = Date.now();
canvasTimerIndex = Math.clamp(canvasTimerIndex + 1, 0, UPDATE_TIMES.length - 1);
return {
requiresUpdate: true,
updateTime: canvasElementUpdateTime,
timerIndex: canvasTimerIndex,
};
}
return {
requiresUpdate: false,
updateTime: canvasElementUpdateTime,
timerIndex: canvasTimerIndex
};
}
/*
* File: multi-column-markdown/src/utilities/cssDefinitions.ts
* Created Date: Wednesday, February 16th 2022, 11:09:06 am
* Author: Cameron Robinson
*
* Copyright (c) 2022 Cameron Robinson
*/
var MultiColumnLayoutCSS;
(function (MultiColumnLayoutCSS) {
MultiColumnLayoutCSS["RegionRootContainerDiv"] = "mcm-column-root-container";
MultiColumnLayoutCSS["RegionErrorContainerDiv"] = "mcm-column-error-region-wrapper";
MultiColumnLayoutCSS["RegionContentContainerDiv"] = "mcm-column-region-wrapper";
MultiColumnLayoutCSS["RegionColumnContainerDiv"] = "mcm-column-parent-container";
MultiColumnLayoutCSS["ColumnDualElementContainer"] = "mcm-column-element-wrapper";
MultiColumnLayoutCSS["OriginalElementType"] = "mcm-original-column-element";
MultiColumnLayoutCSS["ClonedElementType"] = "mcm-cloned-column-element";
MultiColumnLayoutCSS["ContentOverflowAutoScroll_X"] = "mcm-content-overflow-auto-scroll-x";
MultiColumnLayoutCSS["ContentOverflowAutoScroll_Y"] = "mcm-content-overflow-auto-scroll-y";
MultiColumnLayoutCSS["ContentOverflowHidden_X"] = "mcm-content-overflow-hidden-x";
MultiColumnLayoutCSS["ContentOverflowHidden_Y"] = "mcm-content-overflow-hidden-y";
MultiColumnLayoutCSS["AlignmentLeft"] = "mcm-content-alignment-left";
MultiColumnLayoutCSS["AlignmentCenter"] = "mcm-content-alignment-center";
MultiColumnLayoutCSS["AlignmentRight"] = "mcm-content-alignment-right";
MultiColumnLayoutCSS["TableAlignment"] = "mcm-table-alignment";
MultiColumnLayoutCSS["NoFlexShrink"] = "mcm-no-flex-shrink";
MultiColumnLayoutCSS["ReflowContainerDiv"] = "mcm-doc-reflow-container";
MultiColumnLayoutCSS["ErrorRegionPadding"] = "mcm-column-error-padding";
// ------------------------------------------------------ //
MultiColumnLayoutCSS["SingleColumnSmall"] = "mcm-single-column-small";
MultiColumnLayoutCSS["SingleColumnMed"] = "mcm-single-column-medium";
MultiColumnLayoutCSS["SingleColumnLarge"] = "mcm-single-column-large";
MultiColumnLayoutCSS["SingleColumnFull"] = "mcm-single-column-full";
MultiColumnLayoutCSS["SingleColumnLeftLayout"] = "mcm-singlecol-layout-left";
MultiColumnLayoutCSS["SingleColumnCenterLayout"] = "mcm-singlecol-layout-center";
MultiColumnLayoutCSS["SingleColumnRightLayout"] = "mcm-singlecol-layout-right";
// ------------------------------------------------------ //
MultiColumnLayoutCSS["TwoEqualColumns"] = "mcm-two-equal-columns";
MultiColumnLayoutCSS["TwoColumnSmall"] = "mcm-two-column-small";
MultiColumnLayoutCSS["TwoColumnLarge"] = "mcm-two-column-large";
// ------------------------------------------------------ //
MultiColumnLayoutCSS["ThreeEqualColumns"] = "mcm-three-equal-columns";
MultiColumnLayoutCSS["ThreeColumn_Large"] = "mcm-three-column-large";
MultiColumnLayoutCSS["ThreeColumn_Small"] = "mcm-three-column-small";
})(MultiColumnLayoutCSS || (MultiColumnLayoutCSS = {}));
var MultiColumnStyleCSS;
(function (MultiColumnStyleCSS) {
MultiColumnStyleCSS["RegionErrorMessage"] = "mcm-column-error-message";
MultiColumnStyleCSS["RegionSettings"] = "mcm-column-settings-wrapper";
MultiColumnStyleCSS["RegionContent"] = "mcm-column-content-wrapper";
MultiColumnStyleCSS["RegionEndTag"] = "mcm-column-end-tag-wrapper";
MultiColumnStyleCSS["ColumnEndTag"] = "mcm-column-break-tag-wrapper";
MultiColumnStyleCSS["RegionShadow"] = "mcm-region-shadow";
MultiColumnStyleCSS["ColumnShadow"] = "mcm-column-shadow";
MultiColumnStyleCSS["ColumnBorder"] = "mcm-column-border";
MultiColumnStyleCSS["ColumnContent"] = "mcm-column-div";
MultiColumnStyleCSS["SmallFont"] = "mcm-small-font-message";
})(MultiColumnStyleCSS || (MultiColumnStyleCSS = {}));
var ObsidianStyleCSS;
(function (ObsidianStyleCSS) {
ObsidianStyleCSS["RenderedMarkdown"] = "markdown-rendered";
})(ObsidianStyleCSS || (ObsidianStyleCSS = {}));
/**
* File: /src/utilities/elementRenderTypeParser.ts *
* Author: Cameron Robinson *
* *
* Copyright (c) 2023 Cameron Robinson *
*/
function getElementRenderType(element) {
if (isEmbededImage(element) === true) {
return "imageEmbed";
}
if (isButtonPlugin_CrossCompatibilty(element) === true) {
return "buttonPlugin";
}
if (isTasksPlugin(element) === true) {
return "tasksPlugin";
}
/**
* The Dataview plugin needs to be constantly checked if the clone should be
* updated but should not always update the "dual render" aspect, so we add
* a special case for that plugin and maybe others in the future.
*/
if (hasDataview(element) === true) {
return "dataviewPlugin";
}
else if (isPDFEmbed(element)) {
return "pdfEmbed";
}
else if (isInternalEmbed(element)) {
return "internalEmbed";
}
/**
* Some types of content are rendered in canvases which are not rendered properly
* when we clone the original node. Here we are flagging the element as a canvas
* element so we can clone the canvas to a copy element within the region.
*
*/
if (hasDataviewJSCanvas(element) === true) {
return "dataviewJSCanvasEmbed";
}
if (hasDataviewJS(element) === true) {
return "dataviewJSEmbed";
}
if (hasDataviewInline(element) === true) {
return "dataviewInlineQuery";
}
/**
* Look for specific kinds of elements by their CSS class names here. These
* are going to be brittle links as they rely on other plugin definitions but
* as this is only adding in extra compatability to the plugins defined here
* it should be ok.
*
* These may be classes on one of the simple elements (such as a paragraph)
* that we search for below so need to look for these first.
*/
if (hasDiceRoller(element) === true) {
return "diceRoller";
}
else if (hasCopyButton(element) === true) {
return "calloutCopyButton";
}
else if (hasAdmonitionFold(element) === true) {
return "admonitionFold";
}
/**
* This checks for special types of elements that should be rendered normally. Is
* slightly redundant with next check but differentiates between types of ements
* being checked.
*/
if (hasAdmonition(element) === true) {
return "admonition";
}
else if (isIFrame(element) === true) {
return "iFrameEmbed";
}
else if (isCustomIFrame(element) === true) {
return "customFramePlugin";
}
/**
* If we didnt find a special element we want to check for simple elements
* such as paragraphs or lists. In the current implementation we only set up
* the special case for "specialRender" elements so this *should* be saving
* some rendering time by setting these tags properly.
*/
if (hasParagraph(element) ||
hasHeader(element) ||
hasList(element) ||
isHorizontalRule(element) ||
isTable(element)) {
return "basicElement";
}
// If still nothing found we return other as the default response if nothing else found.
return "specialRender";
}
function hasParagraph(element) {
return element.innerHTML.startsWith("<p");
}
function hasHeader(element) {
if (element.innerHTML.startsWith("<h1") ||
element.innerHTML.startsWith("<h2") ||
element.innerHTML.startsWith("<h3") ||
element.innerHTML.startsWith("<h4") ||
element.innerHTML.startsWith("<h5") ||
element.innerHTML.startsWith("<h6")) {
return true;
}
return false;
}
function hasList(element) {
if (element.innerHTML.startsWith("<ul") ||
element.innerHTML.startsWith("<ol")) {
return true;
}
return false;
}
function hasCopyButton(element) {
return element.getElementsByClassName("copy-code-button").length !== 0 ||
element.getElementsByClassName("admonition-content-copy").length !== 0;
}
function hasDiceRoller(element) {
return element.getElementsByClassName("dice-roller").length !== 0;
}
function hasAdmonition(element) {
return element.getElementsByClassName("admonition").length !== 0;
}
function isIFrame(element) {
if (element.children.length > 0) {
return element.firstChild.nodeName.toLowerCase() === "iframe";
}
return false;
}
function isTasksPlugin(element) {
return element.hasClass("block-language-tasks") ||
element.getElementsByClassName("block-language-tasks").length !== 0;
}
function isHorizontalRule(element) {
return element.innerHTML.startsWith("<hr");
}
function isTable(element) {
return element.innerHTML.startsWith("<table");
}
function hasAdmonitionFold(element) {
return element.getElementsByClassName("callout-fold").length !== 0;
}
function hasDataview(element) {
let isDataview = element.getElementsByClassName("block-language-dataview").length !== 0;
return isDataview;
}
function hasDataviewInline(element) {
let isDataview = element.getElementsByClassName("dataview-inline-query").length !== 0;
return isDataview;
}
function hasDataviewJSCanvas(element) {
let isDataviewJS = element.getElementsByClassName("block-language-dataviewjs").length !== 0;
let canvas = searchChildrenForNodeType(element, "canvas");
/**
* This means only dataviewJS chart canvas elements should be rendered properly. Other canvases will
* need thier own case put in or the restriction removed after testing.
*/
return canvas !== null && isDataviewJS;
}
function hasDataviewJS(element) {
let isDataviewJS = element.getElementsByClassName("block-language-dataviewjs").length !== 0;
return isDataviewJS;
}
function isInternalEmbed(element) {
let isEmbed = element.getElementsByClassName("internal-embed").length !== 0;
return isEmbed;
}
function isPDFEmbed(element) {
let isPDFEmbed = element.getElementsByClassName("pdf-embed").length !== 0;
return isPDFEmbed;
}
function getHeadingCollapseElement(element) {
if (element === null) {
return null;
}
let childElements = element.getElementsByClassName("heading-collapse-indicator");
if (childElements.length === 1) {
return childElements[0];
}
if (childElements.length > 1) ;
return null;
}
function isCustomIFrame(element) {
let isFrame = element.getElementsByClassName("custom-frames-frame").length !== 0;
return isFrame;
}
function isButtonPlugin_CrossCompatibilty(element) {
if (element.hasClass("block-language-button")) {
return true;
}
let buttonPluginBlock = element.getElementsByClassName("block-language-button")[0];
if (buttonPluginBlock) {
return true;
}
return false;
}
function isEmbededImage(element) {
let img = element.getElementsByTagName("img")[0];
if (img === null ||
img === undefined) {
return false;
}
return true;
}
/**
* File: /src/dom_manager/regional_managers/RegionManager.ts *
* Created Date: Sunday, May 22nd 2022, 7:49 pm *
* Author: Cameron Robinson *
* *
* Copyright (c) 2022 Cameron Robinson *
*/
class RegionManager {
get numberOfChildren() {
return this.domList.length;
}
get regionParent() {
return this._regionParent;
}
set regionParent(value) {
this._regionParent = value;
}
get errorManager() {
return this._errorManager;
}
set errorManager(value) {
this._errorManager = value;
}
updateErrorManager(newManager, rootElement) {
while (rootElement.children.length > 0) {
rootElement.childNodes.forEach(child => {
rootElement.removeChild(child);
});
}
this._errorManager = newManager;
this._errorManager.setRegionRootElement(rootElement);
}
constructor(data) {
this.domList = [];
this.domObjectMap = new Map();
this.regionalSettings = getDefaultMultiColumnSettings();
this.domList = data.domList;
this.domObjectMap = data.domObjectMap;
this.regionParent = data.regionParent;
this.fileManager = data.fileManager;
this.regionalSettings = data.regionalSettings;
this.regionKey = data.regionKey;
this.errorManager = data.errorManager;
}
getRegionData() {
return {
domList: this.domList,
domObjectMap: this.domObjectMap,
regionParent: this.regionParent,
fileManager: this.fileManager,
regionalSettings: this.regionalSettings,
regionKey: this.regionKey,
rootElement: null,
errorManager: this.errorManager
};
}
/**
* Adds a new object to the region by finding where it should be relative to its siblings.
* @param siblingsAbove The Markdown text rendered elements for sibilings above this element in the dom
* @param siblingsBelow The Markdown text rendered elements for sibilings below this element in the dom
* @param obj The object to add.
* @returns Returns the index at which the object has been added.
*/
addObject(siblingsAbove, siblingsBelow, obj) {
let nextObj = siblingsBelow.children[0];
let addAtIndex = siblingsAbove.children.length;
if (siblingsAbove.children.length > 0) {
/**
* We want to find the first sibling withouth "" for an inner text so we can use that to anchor our
* element into the domList. For most items the first element before our new element will have the proper
* innerText. Sometimes other elements are empty and were causing issues.
*
* Now we loop back through the previous siblings looking for the first one with a valid inner text and using that
* as the anchor and offsetting our addAtIndex by the number of empty string elements we found.
*/
let prevSiblingInnerText = "";
let prevSiblingOffset = 0;
for (let i = siblingsAbove.children.length - 1; i >= 0; i--) {
let obj = siblingsAbove.children[i];
if (obj.innerText !== "") {
prevSiblingInnerText = obj.innerText;
break;
}
prevSiblingOffset++;
}
for (let i = this.domList.length - 1; i >= 0; i--) {
if (this.domList[i].nodeKey === prevSiblingInnerText) {
addAtIndex = i + 1 + prevSiblingOffset;
break;
}
}
}
let nextElIndex = addAtIndex;
if (nextObj !== undefined) {
nextObj.innerText;
for (let i = addAtIndex; i < this.domList.length; i++) {
if (this.domList[i].nodeKey === nextObj.innerText.trim()) {
nextElIndex = i;
break;
}
}
}
// console.log(" Prev: ", Array.from(siblingsAbove.children).slice(-3), "Adding: ", obj.originalElement, " Next: ", siblingsBelow.children[0], "Overwriting:", this.domList.slice(addAtIndex, nextElIndex));
this.domList.splice(addAtIndex, nextElIndex - addAtIndex, obj);
this.domObjectMap.set(obj.UID, obj);
// /**
// * Make a copy of the list to log, only because
// * console log updates its references with updates in memory.
// */
// let x = this.domList.slice(0);
// console.log(x);
return addAtIndex;
}
addObjectAtIndex(obj, index) {
this.domList.splice(index, 0, obj);
this.domObjectMap.set(obj.UID, obj);
}
removeObject(objectUID) {
// /**
// * Make a copy of the list to log
// */
// let x = domList.slice(0);
// console.log(x);
// Get the object by key, remove it from the map and then
// from the list.
let obj = this.domObjectMap.get(objectUID);
this.domObjectMap.delete(objectUID);
if (obj === undefined) {
return;
}
if (this.domList.contains(obj)) {
this.domList.remove(obj);
}
if (this.domList.length === 0 && this.fileManager !== null) {
this.fileManager.removeRegion(this.regionKey);
}
// x = domList.slice(0);
// console.log(x);
}
updateElementTag(objectUID, newTag) {
let obj = this.domObjectMap.get(objectUID);
obj.tag = newTag;
}
setRegionalSettings(regionSettings) {
this.regionalSettings = regionSettings;
}
/**
* Creates an object containing all necessary information for the region
* to be rendered to the preview pane.
*
* @returns a MultiColumnRenderData object with the root DOM element, settings object, and
* all child objects in the order they should be rendered.
*/
getRegionRenderData() {
return {
parentRenderElement: this.regionParent,
parentRenderSettings: this.regionalSettings,
domObjects: this.domList
};
}
/**
* This fuction is called when a start tag is removed from view meaning
* our parent element storing the multi-column region is removed. It
* removes the CSS class from all of the elements so they will be
* re-rendered in the preview window.
*/
displayOriginalElements() {
for (let i = 0; i < this.domList.length; i++) {
if (this.domList[i].originalElement) {
this.domList[i].originalElement.removeClasses([MultiColumnStyleCSS.RegionEndTag,
MultiColumnStyleCSS.ColumnEndTag,
MultiColumnStyleCSS.RegionSettings,
MultiColumnStyleCSS.RegionContent]);
if (this.domList[i].originalElement.parentElement) {
this.domList[i].originalElement.parentElement.removeChild(this.domList[i].originalElement);
}
}
}
}
getID() {
return this.regionKey;
}
updateRenderedMarkdown() {
/**
* This function acts as the update loop for the multi-column regions.
* Here we loop through all of the elements within the rendered region and
* potentially update how things are rendered. We need to do this for
* compatability with other plugins.
*
* If the multi-column region is rendered before other plugins that effect
* content within the region our rendered data may not properly display
* the content from the other plugin. Here we loop through the elements
* after all plugins have had a chance to run and can make changes to the
* DOM at this point.
*/
for (let i = 0; i < this.domList.length; i++) {
let elementType = this.domList[i].elementType;
if (elementType === "unRendered") {
continue;
}
/**
* If the element is not currently a special render element we check again
* as the original element may have been updated.
*/
if (elementType === "undefined" ||
elementType === "basicElement" ||
elementType === "specialRender") {
// If the new result returns as a special renderer we update so
// this wont run again for this item.
elementType = getElementRenderType(this.domList[i].originalElement);
}
let taskListObj = this.domList[i];
/**
* Here we check for special cases
*/
if (taskListObj &&
elementType === "dataviewJSEmbed") {
if (this.domList[i].clonedElementReadyForUpdate()) {
cloneElement(this.domList[i]);
this.fixClonedCheckListButtons(this.domList[i], true);
}
else {
this.fixClonedCheckListButtons(this.domList[i]);
}
continue;
}
if (taskListObj &&
elementType === "basicElement") {
this.fixClonedCheckListButtons(this.domList[i]);
continue;
}
if (elementType === "basicElement") {
this.domList[i].elementType = "basicElement";
continue;
}
if (elementType === "imageEmbed") { //ElementRenderType.fixedElementRender) {
this.domList[i].elementType = elementType;
continue;
}
if (elementType === "buttonPlugin") {
processButtonPluginUpdate(this.domList[i]);
continue;
}
if (elementType === "pdfEmbed") {
this.domList[i].elementType = elementType;
this.setUpDualRender(this.domList[i]);
continue;
}
if (elementType === "diceRoller" ||
elementType === "admonitionFold" ||
elementType === "calloutCopyButton" ||
elementType === "dataviewPlugin" ||
elementType === "internalEmbed" ||
elementType === "dataviewJSCanvasEmbed" ||
elementType === "dataviewJSEmbed" ||
elementType === "dataviewInlineQuery" ||
elementType === "tasksPlugin") {
this.domList[i].elementType = elementType;
this.setUpDualRender(this.domList[i]);
continue;
}
}
}
/**
* This function takes in the original element and its clone and checks if
* the element contains a task-list-item class. If so it loops through all
* items in the list and fixes their checkboxes to properly fire an event.
* The new checkbox calls the click function on the original checkbox so
* compatability with other plugins *should* remain.
* @param domElement
* @param initalizeCheckboxes
*/
fixClonedCheckListButtons(domElement, initalizeCheckboxes = false) {
if (domElement.originalElement === null || domElement.clonedElement === null) {
return;
}
let element = domElement.originalElement;
let clonedElement = domElement.clonedElement;
let clonedListCheckboxes = Array.from(clonedElement.getElementsByClassName("task-list-item"));
let originalListCheckboxes = Array.from(element.getElementsByClassName("task-list-item"));
if (initalizeCheckboxes === true) {
domElement.originalCheckboxes = originalListCheckboxes;
// When we initalize we remove the old input checkbox that contains
// the weird callback situation causing the bug. Then we create a new
// checkbox to replace it and set it up to fire the click event on
// the original checkbox so functionality is restored.
for (let i = 0; i < originalListCheckboxes.length; i++) {
const checkbox = createEl('input');
let originalInput = originalListCheckboxes[i].getElementsByTagName("input")[0];
let isChecked = false;
if (originalInput) {
isChecked = originalInput.checked;
}
else {
console.debug("Could not find original checkbox. Is it null?");
}
let oldCheckbox = TaskListDOMObject.getChildCheckbox(clonedListCheckboxes[i]);
clonedListCheckboxes[i].replaceChild(checkbox, oldCheckbox);
checkbox.checked = isChecked;
checkbox.addClass('task-list-item-checkbox');
checkbox.type = 'checkbox';
checkbox.onClickEvent(() => {
domElement.checkboxClicked(i);
if (checkbox.checked) {
clonedListCheckboxes[i].addClass("is-checked");
clonedListCheckboxes[i].setAttr("data-task", "x");
}
else {
clonedListCheckboxes[i].removeClass("is-checked");
clonedListCheckboxes[i].setAttr("data-task", " ");
}
});
}
}
else {
// Whenever we reach this point we update our list of original checkboxes
// that may be different from our cache. This is due to how obsidian
// changes the DOM underneath us so we need to constantly update our cache.
domElement.originalCheckboxes = originalListCheckboxes;
}
// When the Tasks plugin is installed the cloned copy of the original element contains
// an extra element for some reason. If this occurs for other reasons here we adjust
// that to keep the clone the same as the original.
if (clonedListCheckboxes.length > originalListCheckboxes.length) {
for (let i = originalListCheckboxes.length; i < clonedListCheckboxes.length; i++) {
try {
clonedListCheckboxes[i].detach();
}
catch (error) {
console.debug("No child found when removing from list.");
}
}
}
}
setUpDualRender(domElement) {
/**
* If our element is of "specialRender" type it *may* need to be rendered
* using the original element rather than a copy. For example, an element
* may have an onClick event that would not get coppied to the clone.
*
* If we just moved these elements into the region it would get
* moved back out into the original location in the DOM by obsidian
* when scrolling or when the file is updated. On the next refresh it
* would be moved back but that can lead to a region jumping
* around as the item is moved in and out.
*
* Here we set up the div to contain the element and create
* a visual only clone of it. The clone will only be visible
* when the original is not in the multi-column region so it
* saves us from the visual noise of the region jumping around.
*/
let originalElement = domElement.originalElement;
let clonedElement = domElement.clonedElement;
let containerElement = domElement.elementContainer;
// Get height of the original and cloned element. If the element is not currently rendered
// it will have 0 height so we need to temporarily render it to get the height.
let originalElementHeight = getElementClientHeight(originalElement, containerElement);
let clonedElementHeight = getElementClientHeight(clonedElement, containerElement);
if (domElement.elementType === "pdfEmbed") {
updatePDFEmbed(domElement);
return;
}
if (domElement.elementType === "dataviewJSCanvasEmbed") {
reRenderDataviewJS(domElement);
return;
}
/**
* We only want to clone the element once to reduce GC. But if the cloned
* element's height is not equal to the original element, this means the
* item element has been updated somewhere else without the dom being
* refreshed. This can occur when elements are updated by other plugins,
* such as Dataview.
*/
if ((clonedElement === null ||
Math.abs(clonedElementHeight - originalElementHeight) > 10 ||
domElement.clonedElementReadyForUpdate() === true)) {
// console.log("Updating Cloned Element.", ElementRenderType[domElement.elementType], clonedElementHeight, originalElementHeight)
// Update clone and reference.
cloneElement(domElement);
}
/**
* If the container element has less than 2 children we need to move the
* original element back into it. However some elements constantly get moved
* in and out causing some unwanted behavior. Those element will be tagged
* as specialSingleElementRender so we ignore those elements here.
*/
if (domElement.elementContainer.children.length < 2 &&
domElement.elementType !== "dataviewPlugin" &&
domElement.elementType !== "internalEmbed" &&
domElement.elementType !== "dataviewJSEmbed") {
// console.log("Updating dual rendering.", domElement, domElement.originalElement.parentElement, domElement.originalElement.parentElement?.childElementCount);
// Make sure our CSS is up to date.
originalElement.addClass(MultiColumnLayoutCSS.OriginalElementType);
clonedElement.addClass(MultiColumnLayoutCSS.ClonedElementType);
clonedElement.removeClasses([MultiColumnStyleCSS.RegionContent, MultiColumnLayoutCSS.OriginalElementType]);
for (let i = containerElement.children.length - 1; i >= 0; i--) {
containerElement.children[i].detach();
}
containerElement.appendChild(originalElement);
containerElement.appendChild(clonedElement);
}
}
/**
* Sets up the CSS classes and the number of columns based on the passed settings.
* @param settings The user defined settings that determine what CSS is set here.
* @param multiColumnParent The parent object that the column divs will be created under.
* @returns The list of column divs created under the passed parent element.
*/
getColumnContentDivs(settings, multiColumnParent) {
let columnContentDivs = [];
if (typeof settings.columnSize === "string" &&
isColumnLayout(settings.columnSize) &&
(settings.numberOfColumns === 2 || settings.numberOfColumns === 3)) {
settings.columnSize = validateColumnLayout(settings.columnSize);
multiColumnParent.removeClass(MultiColumnLayoutCSS.ContentOverflowAutoScroll_X);
multiColumnParent.addClass(MultiColumnLayoutCSS.ContentOverflowHidden_X);
buildStandardLayouts(settings, multiColumnParent, columnContentDivs);
setMaxColumnHeight();
return columnContentDivs;
}
if (typeof settings.columnSize === "string" &&
isColumnLayout(settings.columnSize) &&
settings.columnSize === "standard" &&
settings.numberOfColumns > 3) {
settings.columnSize = validateColumnLayout(settings.columnSize);
multiColumnParent.removeClass(MultiColumnLayoutCSS.ContentOverflowAutoScroll_X);
multiColumnParent.addClass(MultiColumnLayoutCSS.ContentOverflowHidden_X);
buildEqualLayout(settings, multiColumnParent, columnContentDivs);
setMaxColumnHeight();
return columnContentDivs;
}
let columnSizes = [];
// If the user has defined the widths individually then we just need to create
// each column individually with each width size.
if (Array.isArray(settings.columnSize)) {
columnSizes = settings.columnSize.slice();
}
else {
calcColumnSizes(settings, columnSizes);
}
if (columnSizes.length === 0) {
columnSizes.push(HTMLSizing.create().setWidth(50).setUnits("%"));
}
for (let i = 0; i < settings.numberOfColumns; i++) {
let sizing = getIndexedClampedArrayValue(i, columnSizes);
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent}`,
attr: { "style": `width: ${sizing.toString()}` }
}));
if (i !== settings.numberOfColumns - 1) {
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(0, settings) }
});
}
}
for (let i = 0; i < columnContentDivs.length; i++) {
columnContentDivs[i].addClass(MultiColumnLayoutCSS.NoFlexShrink);
}
setMaxColumnHeight();
return columnContentDivs;
function setMaxColumnHeight() {
if (settings.columnHeight !== null) {
multiColumnParent.removeClass(MultiColumnLayoutCSS.ContentOverflowHidden_Y);
multiColumnParent.addClass(MultiColumnLayoutCSS.ContentOverflowAutoScroll_Y);
columnContentDivs.forEach((column) => {
column.style.height = settings.columnHeight.toString();
column.style.maxHeight = settings.columnHeight.toString();
column.style.minHeight = settings.columnHeight.toString();
});
}
}
}
}
function createErrorElement(errorText, alt = "", src = "") {
let errorEl = createDiv({
cls: "internal-embed markdown-embed inline-embed is-loaded",
attr: {
"tabindex": "-1",
"contenteditable": "false"
}
});
errorEl.setAttr("alt", alt);
errorEl.setAttr("src", `app://obsidian.md/${src}`);
errorEl.appendChild(createDiv({
"cls": "embed-title markdown-embed-title",
}));
let contentEl = errorEl.createDiv({
"cls": `markdown-embed-content`,
});
let paragraph = contentEl.createEl("p", {
"cls": `${MultiColumnStyleCSS.RegionErrorMessage}, ${MultiColumnStyleCSS.SmallFont}`
});
paragraph.innerText = errorText;
return errorEl;
}
function updatePDFEmbed(domElement) {
// if(domElement.canvasReadyForUpdate() === false) {
// return
// }
domElement.originalElement;
let clonedElement = domElement.clonedElement;
domElement.elementContainer;
for (let i = clonedElement.children.length - 1; i >= 0; i--) {
clonedElement.children[i].detach();
}
clonedElement.appendChild(createErrorElement("Due to an update to Obsidian's PDF viewer, PDF embeds are currently not supported.\nSorry for the inconvienence."));
return;
}
function reRenderDataviewJS(domElement) {
if (domElement.canvasReadyForUpdate() === false) {
return;
}
let originalElement = domElement.originalElement;
let clonedElement = domElement.clonedElement;
let containerElement = domElement.elementContainer;
containerElement.appendChild(originalElement);
if (clonedElement !== null && clonedElement.parentElement === containerElement) {
containerElement.removeChild(clonedElement);
}
function cloneCanvas(originalCanvas) {
//create a new canvas
let clonedCanvas = originalCanvas.cloneNode(true);
let context = clonedCanvas.getContext('2d');
//set dimensions
clonedCanvas.width = originalCanvas.width;
clonedCanvas.height = originalCanvas.height;
if (clonedCanvas.width === 0 || clonedCanvas.height === 0) {
// Dont want to render if the width is 0 as it throws an error
// would happen if the old canvas hasnt been rendered yet.
return clonedCanvas;
}
//apply the old canvas to the new one
context.drawImage(originalCanvas, 0, 0);
//return the new canvas
return clonedCanvas;
}
let canvas = searchChildrenForNodeType(originalElement, "canvas");
if (canvas !== null) {
domElement.updateClonedElement(originalElement.cloneNode(true));
clonedElement = domElement.clonedElement;
clonedElement.addClass(MultiColumnLayoutCSS.ClonedElementType);
clonedElement.removeClasses([MultiColumnStyleCSS.RegionContent, MultiColumnLayoutCSS.OriginalElementType]);
containerElement.appendChild(clonedElement);
for (let i = clonedElement.children.length - 1; i >= 0; i--) {
clonedElement.children[i].detach();
}
clonedElement.appendChild(cloneCanvas(canvas));
}
containerElement.removeChild(originalElement);
containerElement.appendChild(clonedElement);
}
function calcColumnSizes(settings, columnSizes) {
let layout = settings.columnSize;
if (settings.numberOfColumns === 2) {
switch (layout) {
case ("standard"):
case ("middle"):
case ("center"):
case ("third"):
columnSizes.push(HTMLSizing.create().setWidth(50).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(50).setUnits("%"));
break;
case ("left"):
case ("first"):
columnSizes.push(HTMLSizing.create().setWidth(75).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(25).setUnits("%"));
break;
case ("right"):
case ("second"):
case ("last"):
columnSizes.push(HTMLSizing.create().setWidth(25).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(75).setUnits("%"));
break;
}
return;
}
if (settings.numberOfColumns === 3) {
switch (layout) {
case ("standard"):
columnSizes.push(HTMLSizing.create().setWidth(33).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(33).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(33).setUnits("%"));
break;
case ("left"):
case ("first"):
columnSizes.push(HTMLSizing.create().setWidth(50).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(25).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(25).setUnits("%"));
break;
case ("middle"):
case ("center"):
case ("second"):
columnSizes.push(HTMLSizing.create().setWidth(25).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(50).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(25).setUnits("%"));
break;
case ("right"):
case ("third"):
case ("last"):
columnSizes.push(HTMLSizing.create().setWidth(25).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(25).setUnits("%"));
columnSizes.push(HTMLSizing.create().setWidth(50).setUnits("%"));
break;
}
}
}
function getElementClientHeight(element, parentRenderElement) {
let height = element.clientHeight;
if (height === 0) {
parentRenderElement.appendChild(element);
height = element.clientHeight;
parentRenderElement.removeChild(element);
}
return height;
}
function buildEqualLayout(settings, multiColumnParent, columnContentDivs) {
let percent = Math.ceil(100 / settings.numberOfColumns);
for (let i = 0; i < settings.numberOfColumns; i++) {
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent}`,
attr: { "style": `width: ${percent}%` }
}));
if (i !== settings.numberOfColumns - 1) {
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(0, settings) }
});
}
}
}
function buildStandardLayouts(settings, multiColumnParent, columnContentDivs) {
let layout = settings.columnSize;
if (settings.numberOfColumns === 2) {
switch (layout) {
case ("standard"):
case ("middle"):
case ("center"):
case ("third"):
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoEqualColumns}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(0, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoEqualColumns}`
}));
break;
case ("left"):
case ("first"):
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoColumnLarge}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(0, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoColumnSmall}`
}));
break;
case ("right"):
case ("second"):
case ("last"):
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoColumnSmall}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(0, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoColumnLarge}`
}));
break;
}
return;
}
if (settings.numberOfColumns === 3) {
switch (layout) {
case ("standard"):
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeEqualColumns}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(0, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeEqualColumns}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(1, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeEqualColumns}`
}));
break;
case ("left"):
case ("first"):
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Large}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(0, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(1, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
}));
break;
case ("middle"):
case ("center"):
case ("second"):
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(0, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Large}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(1, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
}));
break;
case ("right"):
case ("third"):
case ("last"):
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(0, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
}));
multiColumnParent.createDiv({
cls: `mcm-column-spacer`,
attr: { "style": columnSpacingState(1, settings) }
});
columnContentDivs.push(multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Large}`
}));
break;
}
}
}
function fixOnClick(domElement) {
let originalElement = domElement.originalElement;
let clonedElement = domElement.clonedElement;
let originalButton = originalElement.getElementsByTagName("button")[0];
let clonedButton = clonedElement.getElementsByTagName("button")[0];
if (originalButton === undefined || clonedButton === undefined) {
return;
}
clonedButton.onClickEvent((ev) => {
originalButton.click();
});
}
function cloneElement(domElement) {
let originalElement = domElement.originalElement;
let clonedElement = domElement.clonedElement;
let containerElement = domElement.elementContainer;
domElement.updateClonedElement(originalElement.cloneNode(true));
clonedElement = domElement.clonedElement;
/**
* If we updated the cloned element, we want to also update the
* element rendered in the parent container.
*/
for (let i = containerElement.children.length - 1; i >= 0; i--) {
containerElement.children[i].detach();
}
// Update CSS, we add cloned class and remove classes from originalElement that do not apply.
clonedElement.addClass(MultiColumnLayoutCSS.ClonedElementType);
clonedElement.removeClasses([MultiColumnStyleCSS.RegionContent, MultiColumnLayoutCSS.OriginalElementType]);
containerElement.appendChild(clonedElement);
}
function processButtonPluginUpdate(domObject) {
domObject.elementType = "buttonPlugin";
if (domObject.clonedElementReadyForUpdate() === true) {
cloneElement(domObject);
fixOnClick(domObject);
}
}
const DEFAULT_SETTINGS = {
renderOnMobile: true,
autoLayoutBalanceIterations: 5,
useLivePreviewCache: false,
alignTablesToContentAlignment: true,
renderInlineElErrors: true
};
class MCM_SettingsManager {
constructor() {
this._settings = DEFAULT_SETTINGS;
}
static get shared() {
if (MCM_SettingsManager.local === null) {
MCM_SettingsManager.local = new MCM_SettingsManager();
}
return MCM_SettingsManager.local;
}
get settings() { return this._settings; }
set settings(newVal) {
this.updateTimestamp();
this._settings = newVal;
}
get lastUpdateTimestamp() { return this._lastUpdateTimestamp; }
updateTimestamp() {
this._lastUpdateTimestamp = Date.now();
}
get renderOnMobile() { return this._settings.renderOnMobile; }
get autoLayoutBalanceIterations() { return this._settings.autoLayoutBalanceIterations; }
get useLivePreviewCache() { return this._settings.useLivePreviewCache; }
get alignTablesToContentAlignment() { return this._settings.alignTablesToContentAlignment; }
get renderInlineElErrors() { return this._settings.renderInlineElErrors; }
set renderOnMobile(newVal) {
this.updateTimestamp();
this._settings.renderOnMobile = newVal;
}
set autoLayoutBalanceIterations(newVal) {
this.updateTimestamp();
this._settings.autoLayoutBalanceIterations = newVal;
}
set useLivePreviewCache(newVal) {
this.updateTimestamp();
this._settings.useLivePreviewCache = newVal;
}
set alignTablesToContentAlignment(newVal) {
this.updateTimestamp();
this._settings.alignTablesToContentAlignment = newVal;
}
set renderInlineElErrors(newVal) {
this.updateTimestamp();
this._settings.renderInlineElErrors = newVal;
}
}
MCM_SettingsManager.local = null;
/**
* File: /src/dom_manager/regional_managers/regionDOMManager.ts *
* Created Date: Sunday, May 22nd 2022, 7:46 pm *
* Author: Cameron Robinson *
* *
* Copyright (c) 2022 Cameron Robinson *
*/
class StandardMultiColumnRegionManager extends RegionManager {
renderRegionElementsToScreen() {
this.renderColumnMarkdown(this.regionParent, this.domList, this.regionalSettings);
}
exportRegionElementsToPDF(pdfParentElement) {
// Default set shadow to off for exporting PDFs
let renderSettings = this.regionalSettings;
renderSettings.drawShadow = false;
this.renderColumnMarkdown(pdfParentElement, this.domList.slice(), renderSettings);
}
renderRegionElementsToLivePreview(parentElement) {
this.renderColumnMarkdown(parentElement, this.domList, this.regionalSettings, true);
}
/**
* This function takes in the data for the multi-column region and sets up the
* user defined number of children with the proper css classes to be rendered properly.
*
* @param parentElement The element that the multi-column region will be rendered under.
* @param regionElements The list of DOM objects that will be coppied under the parent object
* @param settings The settings the user has defined for the region.
*/
renderColumnMarkdown(parentElement, regionElements, settings, isLivePreview = false) {
let multiColumnParent = createDiv({
cls: `${MultiColumnLayoutCSS.RegionColumnContainerDiv} \
${MultiColumnLayoutCSS.ContentOverflowAutoScroll_X} \
${MultiColumnLayoutCSS.ContentOverflowHidden_Y};
`
});
/**
* Pass our parent div and settings to parser to create the required
* column divs as children of the parent.
*/
let columnContentDivs = this.getColumnContentDivs(settings, multiColumnParent);
if (settings.drawShadow === true) {
multiColumnParent.addClass(MultiColumnStyleCSS.RegionShadow);
}
for (let i = 0; i < columnContentDivs.length; i++) {
if (shouldDrawColumnBorder(i, settings) === true) {
columnContentDivs[i].addClass(MultiColumnStyleCSS.ColumnBorder);
}
if (settings.drawShadow === true) {
columnContentDivs[i].addClass(MultiColumnStyleCSS.ColumnShadow);
}
}
// Create markdown renderer to parse the passed markdown
// between the tags.
let markdownRenderChild = new obsidian.MarkdownRenderChild(multiColumnParent);
// Remove every other child from the parent so
// we dont end up with multiple sets of data. This should
// really only need to loop once for i = 0 but loop just
// in case.
for (let i = parentElement.children.length - 1; i >= 0; i--) {
parentElement.children[i].detach();
}
parentElement.appendChild(markdownRenderChild.containerEl);
this.appendElementsToColumns(regionElements, columnContentDivs, settings, isLivePreview);
}
appendElementsToColumns(regionElements, columnContentDivs, settings, isLivePreview = false) {
let columnIndex = 0;
for (let i = 0; i < regionElements.length; i++) {
if (regionElements[i].tag === DOMObjectTag.none ||
regionElements[i].tag === DOMObjectTag.columnBreak) {
// If a standard element contains a column break tag and it is set as a pre content break tag we flip our index here.
if (regionElements[i].tag === DOMObjectTag.none &&
regionElements[i].elementIsColumnBreak === ElementColumnBreakType.preBreak &&
(columnIndex + 1) < settings.numberOfColumns) {
columnIndex++;
}
// We store the elements in a wrapper container until we determine
let element = createDiv({
cls: MultiColumnLayoutCSS.ColumnDualElementContainer,
});
if (columnOverflowState(columnIndex, settings) === ContentOverflowType.hidden) {
element.addClass(MultiColumnLayoutCSS.ContentOverflowHidden_X);
}
else {
element.addClass(MultiColumnLayoutCSS.ContentOverflowAutoScroll_X);
}
let alignment = columnAlignmentState(columnIndex, settings);
if (alignment === AlignmentType.center) {
element.addClass(MultiColumnLayoutCSS.AlignmentCenter);
}
else if (alignment === AlignmentType.right) {
element.addClass(MultiColumnLayoutCSS.AlignmentRight);
}
else {
element.addClass(MultiColumnLayoutCSS.AlignmentLeft);
}
let tableAlignment = MCM_SettingsManager.shared.alignTablesToContentAlignment;
if (settings.alignTablesToAlignment !== TableAlignment.useSettingsDefault) {
tableAlignment = settings.alignTablesToAlignment === TableAlignment.align;
}
if (tableAlignment) {
element.addClass(MultiColumnLayoutCSS.TableAlignment);
}
regionElements[i].elementContainer = element;
let elementToAppend = regionElements[i].originalElement;
if (isLivePreview === false) {
let clonedElement = regionElements[i].originalElement.cloneNode(true);
let headingCollapseElement = getHeadingCollapseElement(clonedElement);
if (headingCollapseElement !== null) {
// This removes the collapse arrow from the view if it exists.
headingCollapseElement.detach();
}
regionElements[i].clonedElement = clonedElement;
elementToAppend = clonedElement;
}
element.appendChild(elementToAppend);
if (regionElements[i] instanceof TaskListDOMObject) {
this.fixClonedCheckListButtons(regionElements[i], true);
}
if (element !== null && regionElements[i].tag !== DOMObjectTag.columnBreak) {
columnContentDivs[columnIndex].appendChild(element);
}
/**
* If the tag is a column break we update the column index after
* appending the item to the column div. This keeps the main DOM
* cleaner by removing other items and placing them all within
* a region container.
*/
if (regionElements[i].tag === DOMObjectTag.columnBreak &&
(columnIndex + 1) < settings.numberOfColumns) {
columnIndex++;
}
else if (regionElements[i].tag === DOMObjectTag.none &&
regionElements[i].elementIsColumnBreak === ElementColumnBreakType.postBreak &&
(columnIndex + 1) < settings.numberOfColumns) {
// If a standard element contains a column break tag and it is set as a post content break tag we flip our index here.
columnIndex++;
}
}
}
}
}
/**
* File: /src/dom_manager/regional_managers/singleColumnRegionManager.ts *
* Author: Cameron Robinson *
* *
* Copyright (c) 2023 Cameron Robinson *
*/
class SingleColumnRegionManager extends RegionManager {
renderRegionElementsToScreen() {
this.renderColumnMarkdown(this.regionParent, this.domList, this.regionalSettings);
}
exportRegionElementsToPDF(pdfParentElement) {
// Default set shadow to off for exporting PDFs
let renderSettings = this.regionalSettings;
renderSettings.drawShadow = false;
this.renderColumnMarkdown(pdfParentElement, this.domList.slice(), renderSettings);
}
renderRegionElementsToLivePreview(parentElement) {
this.renderColumnMarkdown(parentElement, this.domList, this.regionalSettings, true);
}
/**
* This function takes in the data for the multi-column region and sets up the
* user defined number of children with the proper css classes to be rendered properly.
*
* @param parentElement The element that the multi-column region will be rendered under.
* @param regionElements The list of DOM objects that will be coppied under the parent object
* @param settings The settings the user has defined for the region.
*/
renderColumnMarkdown(parentElement, regionElements, settings, isLivePreview = false) {
let multiColumnParent = createDiv({
cls: `${MultiColumnLayoutCSS.RegionColumnContainerDiv} \
${MultiColumnLayoutCSS.ContentOverflowAutoScroll_X} \
${MultiColumnLayoutCSS.ContentOverflowHidden_Y};
`
});
if (isLeftLayout(this.regionalSettings.columnPosition)) {
multiColumnParent.addClass(MultiColumnLayoutCSS.SingleColumnLeftLayout);
}
else if (isRightLayout(this.regionalSettings.columnPosition)) {
multiColumnParent.addClass(MultiColumnLayoutCSS.SingleColumnRightLayout);
}
else {
multiColumnParent.addClass(MultiColumnLayoutCSS.SingleColumnCenterLayout);
}
/**
* Pass our parent div and settings to parser to create the required
* column divs as children of the parent.
*/
let columnContentDiv = this.createColumnContentDivs(multiColumnParent);
if (shouldDrawColumnBorder(0, settings) === true) {
columnContentDiv.addClass(MultiColumnStyleCSS.ColumnBorder);
}
if (settings.drawShadow === true) {
columnContentDiv.addClass(MultiColumnStyleCSS.ColumnShadow);
}
// Create markdown renderer to parse the passed markdown
// between the tags.
let markdownRenderChild = new obsidian.MarkdownRenderChild(multiColumnParent);
// Remove every other child from the parent so
// we dont end up with multiple sets of data. This should
// really only need to loop once for i = 0 but loop just
// in case.
for (let i = parentElement.children.length - 1; i >= 0; i--) {
parentElement.children[i].detach();
}
parentElement.appendChild(markdownRenderChild.containerEl);
this.appendElementsToColumns(regionElements, columnContentDiv, settings, isLivePreview);
}
appendElementsToColumns(regionElements, columnContentDiv, settings, isLivePreview = false) {
for (let i = 0; i < regionElements.length; i++) {
if (regionElements[i].tag === DOMObjectTag.none ||
regionElements[i].tag === DOMObjectTag.columnBreak) {
// We store the elements in a wrapper container until we determine
let element = createDiv({
cls: MultiColumnLayoutCSS.ColumnDualElementContainer,
});
regionElements[i].elementContainer = element;
if (columnOverflowState(0, settings) === ContentOverflowType.hidden) {
element.addClass(MultiColumnLayoutCSS.ContentOverflowHidden_X);
}
else {
element.addClass(MultiColumnLayoutCSS.ContentOverflowAutoScroll_X);
}
let alignment = columnAlignmentState(0, settings);
if (alignment === AlignmentType.center) {
element.addClass(MultiColumnLayoutCSS.AlignmentCenter);
}
else if (alignment === AlignmentType.right) {
element.addClass(MultiColumnLayoutCSS.AlignmentRight);
}
else {
element.addClass(MultiColumnLayoutCSS.AlignmentLeft);
}
let tableAlignment = MCM_SettingsManager.shared.alignTablesToContentAlignment;
if (settings.alignTablesToAlignment !== TableAlignment.useSettingsDefault) {
tableAlignment = settings.alignTablesToAlignment === TableAlignment.align;
}
if (tableAlignment) {
element.addClass(MultiColumnLayoutCSS.TableAlignment);
}
let elementToAppend = regionElements[i].originalElement;
if (isLivePreview === false) {
let clonedElement = regionElements[i].originalElement.cloneNode(true);
let headingCollapseElement = getHeadingCollapseElement(clonedElement);
if (headingCollapseElement !== null) {
// This removes the collapse arrow from the view if it exists.
headingCollapseElement.detach();
}
regionElements[i].clonedElement = clonedElement;
elementToAppend = clonedElement;
}
element.appendChild(elementToAppend);
if (regionElements[i] instanceof TaskListDOMObject) {
this.fixClonedCheckListButtons(regionElements[i], true);
}
if (element !== null) {
columnContentDiv.appendChild(element);
}
}
}
}
createColumnContentDivs(multiColumnParent) {
let contentDiv = multiColumnParent.createDiv({
cls: `${MultiColumnStyleCSS.ColumnContent}`
});
if (this.regionalSettings.columnSize === "small") {
contentDiv.addClass(`${MultiColumnLayoutCSS.SingleColumnSmall}`);
}
else if (this.regionalSettings.columnSize === "large") {
contentDiv.addClass(`${MultiColumnLayoutCSS.SingleColumnLarge}`);
}
else if (this.regionalSettings.columnSize === "full") {
contentDiv.addClass(`${MultiColumnLayoutCSS.SingleColumnFull}`);
}
else {
contentDiv.addClass(`${MultiColumnLayoutCSS.SingleColumnMed}`);
}
return contentDiv;
}
}
function isLeftLayout(layout) {
if (layout === "left" ||
layout === "first") {
return true;
}
return false;
}
function isRightLayout(layout) {
if (layout === "right" ||
layout === "third" ||
layout === "last") {
return true;
}
return false;
}
/**
* File: /src/dom_manager/regional_managers/autoLayoutRegionManager.ts *
* Created Date: Sunday, May 22nd 2022, 10:23 pm *
* Author: Cameron Robinson *
* *
* Copyright (c) 2022 Cameron Robinson *
*/
class AutoLayoutRegionManager extends RegionManager {
constructor(data, isLivePreview = false) {
super(data);
this.isLivePreview = false;
this.previousColumnHeights = [];
this.isLivePreview = isLivePreview;
this.docReflow = data.regionalSettings.fullDocReflow;
}
renderRegionElementsToScreen() {
this.renderColumnMarkdown(this.regionParent, this.domList, this.regionalSettings);
}
exportRegionElementsToPDF(pdfParentElement) {
// Default set shadow to off for exporting PDFs
let renderSettings = this.regionalSettings;
renderSettings.drawShadow = false;
this.renderColumnMarkdown(pdfParentElement, this.domList.slice(), renderSettings);
}
renderRegionElementsToLivePreview(parentElement) {
this.renderColumnMarkdown(parentElement, this.domList, this.regionalSettings, true);
}
/**
* This function takes in the data for the multi-column region and sets up the
* user defined number of children with the proper css classes to be rendered properly.
*
* @param parentElement The element that the multi-column region will be rendered under.
* @param regionElements The list of DOM objects that will be coppied under the parent object
* @param settings The settings the user has defined for the region.
*/
renderColumnMarkdown(parentElement, regionElements, settings, isLivePreview = false) {
let multiColumnParent = createDiv({
cls: `${MultiColumnLayoutCSS.RegionColumnContainerDiv} \
${MultiColumnLayoutCSS.ContentOverflowAutoScroll_X} \
${MultiColumnLayoutCSS.ContentOverflowHidden_Y};
`
});
this.columnParent = multiColumnParent;
/**
* Pass our parent div and settings to parser to create the required
* column divs as children of the parent.
*/
this.columnDivs = this.getColumnContentDivs(settings, multiColumnParent);
if (settings.drawShadow === true) {
multiColumnParent.addClass(MultiColumnStyleCSS.RegionShadow);
}
for (let i = 0; i < this.columnDivs.length; i++) {
if (shouldDrawColumnBorder(i, settings) === true) {
this.columnDivs[i].addClass(MultiColumnStyleCSS.ColumnBorder);
}
if (settings.drawShadow === true) {
this.columnDivs[i].addClass(MultiColumnStyleCSS.ColumnShadow);
}
}
// Remove every other child from the parent so
// we dont end up with multiple sets of data. This should
// really only need to loop once for i = 0 but loop just
// in case.
for (let i = parentElement.children.length - 1; i >= 0; i--) {
parentElement.children[i].detach();
}
parentElement.appendChild(multiColumnParent);
this.appendElementsToColumns(regionElements, this.columnDivs, settings, isLivePreview);
}
appendElementsToColumns(regionElements, columnContentDivs, settings, isLivePreview = false) {
function balanceElements() {
let totalHeight = regionElements.map((el, index) => {
// We only want to attempt to update the elementRenderedHeight if it is 0 and if it is not an unrendered element such as a endregion tag.
if (el.elementRenderedHeight === 0 &&
el.tag !== DOMObjectTag.columnBreak &&
el.tag !== DOMObjectTag.endRegion &&
el.tag !== DOMObjectTag.regionSettings &&
el.tag !== DOMObjectTag.startRegion) {
// Add element to rendered div so we can extract the rendered height.
columnContentDivs[0].appendChild(el.originalElement);
el.elementRenderedHeight = el.originalElement.clientHeight;
columnContentDivs[0].removeChild(el.originalElement);
}
return el.elementRenderedHeight;
}).reduce((prev, curr) => { return prev + curr; }, 0);
let maxColumnContentHeight = Math.trunc(totalHeight / settings.numberOfColumns);
for (let i = 0; i < columnContentDivs.length; i++) {
for (let j = columnContentDivs[i].children.length - 1; j >= 0; j--) {
columnContentDivs[i].children[j].detach();
}
}
let columnIndex = 0;
let currentColumnHeight = 0;
function checkShouldSwitchColumns(nextElementHeight) {
if (currentColumnHeight + nextElementHeight > maxColumnContentHeight &&
(columnIndex + 1) < settings.numberOfColumns) {
columnIndex++;
currentColumnHeight = 0;
}
}
for (let i = 0; i < regionElements.length; i++) {
if (regionElements[i].tag === DOMObjectTag.none ||
regionElements[i].tag === DOMObjectTag.columnBreak) {
/**
* Here we check if we need to swap to the next column for the current element.
* If the user wants to keep headings with the content below it we also make sure
* that the last item in a column is not a header element by using the header and
* the next element's height as the height value.
*/
if (hasHeader(regionElements[i].originalElement) === true) { // TODO: Add this as selectable option.
let headerAndNextElementHeight = regionElements[i].elementRenderedHeight;
if (i < regionElements.length - 1) {
headerAndNextElementHeight += regionElements[i + 1].elementRenderedHeight;
}
checkShouldSwitchColumns(headerAndNextElementHeight);
}
else {
checkShouldSwitchColumns(regionElements[i].elementRenderedHeight);
}
currentColumnHeight += regionElements[i].elementRenderedHeight;
/**
* We store the elements in a wrapper container until we determine if we want to
* use the original element or a clone of the element. This helps us by allowing
* us to create a visual only clone while the update loop moves the original element
* into the columns.
*/
let element = createDiv({
cls: MultiColumnLayoutCSS.ColumnDualElementContainer,
});
regionElements[i].elementContainer = element;
if (columnOverflowState(columnIndex, settings) === ContentOverflowType.hidden) {
element.addClass(MultiColumnLayoutCSS.ContentOverflowHidden_X);
}
else {
element.addClass(MultiColumnLayoutCSS.ContentOverflowAutoScroll_X);
}
let alignment = columnAlignmentState(columnIndex, settings);
if (alignment === AlignmentType.center) {
element.addClass(MultiColumnLayoutCSS.AlignmentCenter);
}
else if (alignment === AlignmentType.right) {
element.addClass(MultiColumnLayoutCSS.AlignmentRight);
}
else {
element.addClass(MultiColumnLayoutCSS.AlignmentLeft);
}
let tableAlignment = MCM_SettingsManager.shared.alignTablesToContentAlignment;
if (settings.alignTablesToAlignment !== TableAlignment.useSettingsDefault) {
tableAlignment = settings.alignTablesToAlignment === TableAlignment.align;
}
if (tableAlignment) {
element.addClass(MultiColumnLayoutCSS.TableAlignment);
}
let elementToAppend = regionElements[i].originalElement;
if (isLivePreview === false) {
let clonedElement = regionElements[i].clonedElement;
if (regionElements[i].clonedElement === null) {
clonedElement = regionElements[i].originalElement.cloneNode(true);
let headingCollapseElement = getHeadingCollapseElement(clonedElement);
if (headingCollapseElement !== null) {
// This removes the collapse arrow from the view if it exists.
headingCollapseElement.detach();
}
regionElements[i].clonedElement = clonedElement;
elementToAppend = clonedElement;
}
}
element.appendChild(elementToAppend);
if (regionElements[i] instanceof TaskListDOMObject) {
this.fixClonedCheckListButtons(regionElements[i], true);
}
if (element !== null &&
columnContentDivs[columnIndex] &&
regionElements[i].tag !== DOMObjectTag.columnBreak) {
columnContentDivs[columnIndex].appendChild(element);
regionElements[i].elementRenderedHeight = element.clientHeight;
}
/**
* If the tag is a column break we update the column index after
* appending the item to the column div. This keeps the main DOM
* cleaner by removing other items and placing them all within
* a region container.
*
* Removing the end column tag as an option for now.
*/
// if (regionElements[i].tag === DOMObjectTag.columnBreak &&
// (columnIndex + 1) < settings.numberOfColumns) {
// columnIndex++;
// currentColumnHeight = 0;
// }
}
}
}
/**
* Attempt to balanced the elements. We need to iterate over the elements multiple times because
* our initial balance estimate may not be perfectly balanced due to different column widths causing
* elements within them to be of different heights. This can cause the elements to jump around on
* subsiquent update loops which is not ideal. Here we render the elements to the screen and update
* their height after being rendered into the estimated position.
*
* Once everything is rendered we check all of the column heights against our last iteration and
* if nothing has changed we know we are balanced.
*
* There is probably a better way of accomplishing this task but this works for the time being.
*/
let autoLayoutBalanceIterations = 1;
if (this.isLivePreview === false) {
autoLayoutBalanceIterations = MCM_SettingsManager.shared.autoLayoutBalanceIterations;
}
for (let i = 0; i < autoLayoutBalanceIterations; i++) {
balanceElements();
let balanced = true;
for (let j = 0; j < columnContentDivs.length; j++) {
// If the column heights are undefined we set default to zero so not to encounter an error.
if (!this.previousColumnHeights[j]) {
this.previousColumnHeights.push(0);
}
// if this render height is not the same as the previous height we are still balancing.
if (this.previousColumnHeights[j] !== columnContentDivs[j].clientHeight) {
this.previousColumnHeights[j] = columnContentDivs[j].clientHeight;
balanced = false;
}
}
// if we made it out of the loop and all of the columns are the same height as last update
// we're balanced so we can break out of the loop.
if (balanced === true) {
break;
}
}
}
updateRenderedMarkdown() {
if (this.docReflow === true) {
super.updateRenderedMarkdown();
return;
}
for (let i = 0; i < this.domList.length; i++) {
let el = this.domList[i];
let originalClientHeight = 0;
if (el.originalElement) {
originalClientHeight = el.originalElement.clientHeight;
}
let clonedClientHeight = 0;
if (el.clonedElement) {
clonedClientHeight = el.clonedElement.clientHeight;
}
if (originalClientHeight < clonedClientHeight) {
this.domList[i].elementRenderedHeight = clonedClientHeight;
}
else {
this.domList[i].elementRenderedHeight = originalClientHeight;
}
}
let validColumns = true;
if (this.columnParent !== null && this.columnDivs !== null && this.columnDivs !== undefined &&
this.columnDivs.length === this.regionalSettings.numberOfColumns) {
let totalHeight = this.domList.map((el, index) => {
// We only want to attempt to update the elementRenderedHeight if it is 0 and if it is not an unrendered element such as a endregion tag.
if (el.elementRenderedHeight === 0 &&
el.tag !== DOMObjectTag.columnBreak &&
el.tag !== DOMObjectTag.endRegion &&
el.tag !== DOMObjectTag.regionSettings &&
el.tag !== DOMObjectTag.startRegion) {
// Add element to rendered div so we can extract the rendered height.
this.columnParent.appendChild(el.originalElement);
el.elementRenderedHeight = el.originalElement.clientHeight;
this.columnParent.removeChild(el.originalElement);
}
return el.elementRenderedHeight;
}).reduce((prev, curr) => { return prev + curr; }, 0);
let maxColumnContentHeight = Math.trunc(totalHeight / this.regionalSettings.numberOfColumns);
for (let i = 0; i < this.columnDivs.length - 1; i++) {
let columnHeight = 0;
for (let j = 0; j < this.columnDivs[i].children.length; j++) {
columnHeight += this.columnDivs[i].children[j].clientHeight;
}
if (columnHeight > maxColumnContentHeight) {
validColumns = false;
break;
}
}
}
if (validColumns === false) {
this.renderColumnMarkdown(this.regionParent, this.domList, this.regionalSettings);
}
super.updateRenderedMarkdown();
}
}
/**
* File: /src/dom_manager/regional_managers/reflowRegionManager.ts *
* Created Date: Thursday, May 11th 2023, 9:59 pm *
* Author: Cameron Robinson *
* *
* Copyright (c) 2023 Cameron Robinson *
*/
class ReflowRegionManager extends RegionManager {
renderRegionElementsToScreen() {
this.renderColumnMarkdown(this.regionParent, this.domList, this.regionalSettings);
}
exportRegionElementsToPDF(pdfParentElement) {
// Default set shadow to off for exporting PDFs
let renderSettings = this.regionalSettings;
renderSettings.drawShadow = false;
this.renderColumnMarkdown(pdfParentElement, this.domList.slice(), renderSettings);
}
renderRegionElementsToLivePreview(parentElement) {
this.renderColumnMarkdown(parentElement, this.domList, this.regionalSettings);
}
/**
* This function takes in the data for the multi-column region and sets up the
* user defined number of children with the proper css classes to be rendered properly.
*
* @param parentElement The element that the multi-column region will be rendered under.
* @param regionElements The list of DOM objects that will be coppied under the parent object
* @param settings The settings the user has defined for the region.
*/
renderColumnMarkdown(parentElement, regionElements, settings) {
let verticalColumnParent = createDiv({
cls: ``
});
/**
* Pass our parent div and settings to parser to create the required
* column divs as children of the parent.
*/
// Remove every other child from the parent so
// we dont end up with multiple sets of data. This should
// really only need to loop once for i = 0 but loop just
// in case.
for (let i = parentElement.children.length - 1; i >= 0; i--) {
parentElement.children[i].detach();
}
parentElement.appendChild(verticalColumnParent);
this.appendElementsToColumns(verticalColumnParent, regionElements, settings);
}
appendElementsToColumns(verticalColumnParent, regionElements, settings) {
this.domList.forEach((el, index) => {
// We only want to attempt to update the elementRenderedHeight if it is 0 and if it is not an unrendered element such as a endregion tag.
if (el.elementRenderedHeight === 0 &&
el.tag !== DOMObjectTag.columnBreak &&
el.tag !== DOMObjectTag.endRegion &&
el.tag !== DOMObjectTag.regionSettings &&
el.tag !== DOMObjectTag.startRegion) {
// Add element to rendered div so we can extract the rendered height.
verticalColumnParent.appendChild(el.originalElement);
el.elementRenderedHeight = el.originalElement.clientHeight;
verticalColumnParent.removeChild(el.originalElement);
}
});
let maxColumnContentHeight = settings.columnHeight.sizeValue;
let columnIndex = 0;
let currentColumnHeight = 0;
let divCount = 1;
let colDivsCallback = (settings, multiColumnParent) => {
return this.getColumnContentDivs(settings, multiColumnParent);
};
let columns = getFormattedColumnDivs(settings, verticalColumnParent, colDivsCallback, divCount);
for (let i = 0; i < regionElements.length; i++) {
if (regionElements[i].tag === DOMObjectTag.none ||
regionElements[i].tag === DOMObjectTag.columnBreak) {
/**
* Here we check if we need to swap to the next column for the current element.
* If the user wants to keep headings with the content below it we also make sure
* that the last item in a column is not a header element by using the header and
* the next element's height as the height value.
*/
if (hasHeader(regionElements[i].originalElement) === true) { // TODO: Add this as selectable option.
let headerAndNextElementHeight = regionElements[i].elementRenderedHeight;
if (i < regionElements.length - 1) {
headerAndNextElementHeight += regionElements[i + 1].elementRenderedHeight;
}
checkShouldSwitchColumns(headerAndNextElementHeight);
}
else {
checkShouldSwitchColumns(regionElements[i].elementRenderedHeight);
}
currentColumnHeight += regionElements[i].elementRenderedHeight;
/**
* We store the elements in a wrapper container until we determine if we want to
* use the original element or a clone of the element. This helps us by allowing
* us to create a visual only clone while the update loop moves the original element
* into the columns.
*/
let element = createDiv({
cls: MultiColumnLayoutCSS.ColumnDualElementContainer,
});
regionElements[i].elementContainer = element;
if (columnOverflowState(columnIndex, settings) === ContentOverflowType.hidden) {
element.addClass(MultiColumnLayoutCSS.ContentOverflowHidden_X);
}
else {
element.addClass(MultiColumnLayoutCSS.ContentOverflowAutoScroll_X);
}
let alignment = columnAlignmentState(columnIndex, settings);
if (alignment === AlignmentType.center) {
element.addClass(MultiColumnLayoutCSS.AlignmentCenter);
}
else if (alignment === AlignmentType.right) {
element.addClass(MultiColumnLayoutCSS.AlignmentRight);
}
else {
element.addClass(MultiColumnLayoutCSS.AlignmentLeft);
}
let tableAlignment = MCM_SettingsManager.shared.alignTablesToContentAlignment;
if (settings.alignTablesToAlignment !== TableAlignment.useSettingsDefault) {
tableAlignment = settings.alignTablesToAlignment === TableAlignment.align;
}
if (tableAlignment) {
element.addClass(MultiColumnLayoutCSS.TableAlignment);
}
let clonedElement = regionElements[i].clonedElement;
if (regionElements[i].clonedElement === null) {
clonedElement = regionElements[i].originalElement.cloneNode(true);
let headingCollapseElement = getHeadingCollapseElement(clonedElement);
if (headingCollapseElement !== null) {
// This removes the collapse arrow from the view if it exists.
headingCollapseElement.detach();
}
regionElements[i].clonedElement = clonedElement;
}
element.appendChild(clonedElement);
if (regionElements[i] instanceof TaskListDOMObject) {
this.fixClonedCheckListButtons(regionElements[i], true);
}
if (element !== null &&
columns[columnIndex] &&
regionElements[i].tag !== DOMObjectTag.columnBreak) {
columns[columnIndex].appendChild(element);
regionElements[i].elementRenderedHeight = element.clientHeight;
}
/**
* If the tag is a column break we update the column index after
* appending the item to the column div. This keeps the main DOM
* cleaner by removing other items and placing them all within
* a region container.
*/
if (regionElements[i].tag === DOMObjectTag.columnBreak) {
checkCreateNewColumns();
columnIndex++;
currentColumnHeight = 0;
}
}
}
function checkShouldSwitchColumns(nextElementHeight) {
if (currentColumnHeight + nextElementHeight < maxColumnContentHeight) {
return;
}
checkCreateNewColumns();
columnIndex++;
currentColumnHeight = 0;
}
function checkCreateNewColumns() {
if ((columnIndex + 1) >= columns.length) {
divCount++;
columns = columns.concat(getFormattedColumnDivs(settings, verticalColumnParent, colDivsCallback, divCount));
}
}
}
}
function getFormattedColumnDivs(settings, verticalColumnParent, getColumnContentDivs, divCount) {
let multiColumnParent = verticalColumnParent.createDiv({
cls: `${MultiColumnLayoutCSS.RegionColumnContainerDiv} \
${MultiColumnLayoutCSS.ContentOverflowAutoScroll_X} \
${MultiColumnLayoutCSS.ContentOverflowHidden_Y};
`
});
if (divCount > 1) {
multiColumnParent.addClass(`${MultiColumnLayoutCSS.ReflowContainerDiv}`);
}
let columnDivs = getColumnContentDivs(settings, multiColumnParent);
if (settings.drawShadow === true) {
multiColumnParent.addClass(MultiColumnStyleCSS.RegionShadow);
}
for (let i = 0; i < columnDivs.length; i++) {
if (shouldDrawColumnBorder(i, settings) === true) {
columnDivs[i].addClass(MultiColumnStyleCSS.ColumnBorder);
}
if (settings.drawShadow === true) {
columnDivs[i].addClass(MultiColumnStyleCSS.ColumnShadow);
}
}
return columnDivs;
}
/**
* File: /src/dom_manager/regional_managers/regionManagerContainer.ts *
* Created Date: Sunday, May 22nd 2022, 7:50 pm *
* Author: Cameron Robinson *
* *
* Copyright (c) 2022 Cameron Robinson *
*/
/**
* This class acts as an abstraction for the actual regional manager. It is used to update the
* subclass of RegionalManager depending on user preferences to make rendering more simplified.
*/
class RegionManagerContainer {
constructor(parentFileManager, regionKey, rootElement, regionParent, errorManager) {
this.region = new StandardMultiColumnRegionManager(createDefaultRegionManagerData(regionParent, parentFileManager, regionKey, rootElement, errorManager));
}
getRegion() {
return this.region;
}
setRegionSettings(settingsText) {
let regionalSettings = parseColumnSettings(settingsText);
if (regionalSettings.numberOfColumns === 1) {
regionalSettings = parseSingleColumnSettings(settingsText, regionalSettings);
}
return this.setRegionParsedSettings(regionalSettings);
}
setRegionParsedSettings(regionalSettings) {
this.region.setRegionalSettings(regionalSettings);
if (regionalSettings.numberOfColumns === 1) {
if (this.region instanceof SingleColumnRegionManager === false) {
// console.debug("Converting region to single column.")
this.convertToSingleColumn();
}
}
else if (regionalSettings.autoLayout === true) {
if (this.region instanceof AutoLayoutRegionManager === false) {
// console.debug("Converting region to auto layout.")
this.convertToAutoLayout();
}
}
else if (regionalSettings.fullDocReflow === true) {
if (this.region instanceof ReflowRegionManager === false) {
// console.debug("Converting region to auto layout.")
this.convertToDocReflow();
}
}
else if (regionalSettings.numberOfColumns >= 2) {
if (this.region instanceof StandardMultiColumnRegionManager === false) {
// console.debug("Converting region to standard multi-column")
this.convertToStandardMultiColumn();
}
}
return this.region;
}
convertToSingleColumn() {
let data = this.region.getRegionData();
this.region = new SingleColumnRegionManager(data);
return this.region;
}
convertToStandardMultiColumn() {
let data = this.region.getRegionData();
this.region = new StandardMultiColumnRegionManager(data);
return this.region;
}
convertToAutoLayout() {
let data = this.region.getRegionData();
this.region = new AutoLayoutRegionManager(data);
return this.region;
}
convertToDocReflow() {
let data = this.region.getRegionData();
this.region = new ReflowRegionManager(data);
return this.region;
}
}
function createDefaultRegionManagerData(regionParent, fileManager, regionKey, rootElement, errorManager) {
return {
domList: [],
domObjectMap: new Map(),
regionParent: regionParent,
fileManager: fileManager,
regionalSettings: getDefaultMultiColumnSettings(),
regionKey: regionKey,
rootElement: rootElement,
errorManager: errorManager
};
}
/*
* File: multi-column-markdown/src/domManager.ts
* Created Date: Saturday, January 30th 2022, 3:16:32 pm
* Author: Cameron Robinson
*
* Copyright (c) 2022 Cameron Robinson
*/
/**
* This class handles the global managers keeping track of all open files that
* contain MCM-Regions.
*/
class GlobalDOMManager {
constructor() {
this.managers = new Map();
}
removeFileManagerCallback(key) {
if (this.managers.has(key) === true) {
this.managers.delete(key);
}
}
getFileManager(key) {
let fileManager = null;
if (this.managers.has(key) === true) {
fileManager = this.managers.get(key);
}
else {
fileManager = new FileDOMManager(this, key);
this.managers.set(key, fileManager);
}
return fileManager;
}
getAllFileManagers() {
return Array.from(this.managers.values());
}
}
class FileDOMManager {
constructor(parentManager, fileKey) {
this.regionMap = new Map();
this.hasStartTag = false;
this.parentManager = parentManager;
this.fileKey = fileKey;
}
removeRegion(regionKey) {
let regionContainer = this.regionMap.get(regionKey);
if (regionContainer === undefined) {
return;
}
let regionalManager = regionContainer.getRegion();
regionalManager.displayOriginalElements();
this.regionMap.delete(regionKey);
if (this.regionMap.size === 0) {
this.parentManager.removeFileManagerCallback(this.fileKey);
}
}
createRegionalManager(regionKey, rootElement, errorManager, renderRegionElement) {
let regonalContainer = new RegionManagerContainer(this, regionKey, rootElement, renderRegionElement, errorManager);
this.regionMap.set(regionKey, regonalContainer);
return regonalContainer.getRegion();
}
getRegionalContainer(regionKey) {
let regonalManager = null;
if (this.regionMap.has(regionKey) === true) {
regonalManager = this.regionMap.get(regionKey);
}
return regonalManager;
}
getAllRegionalManagers() {
let containers = Array.from(this.regionMap.values());
let regions = containers.map((curr) => { return curr.getRegion(); });
return regions;
}
setHasStartTag() {
this.hasStartTag = true;
}
getHasStartTag() {
return this.hasStartTag;
}
getNumberOfRegions() {
return this.regionMap.size;
}
checkKeyExists(checkKey) {
return this.regionMap.has(checkKey);
}
}
class RegionErrorManager {
get totalNumMessages() {
return this.errorMessages.length + this.warningMessages.length;
}
constructor(rootElement, initialErrorMessages = []) {
this.errorMessages = [];
this.warningMessages = [];
this.errorMessages = initialErrorMessages;
this.setRegionRootElement(rootElement);
}
addErrorMessage(errorString) {
this.errorMessages.push(errorString);
this.updateErrorView();
}
addWarningMessage(warningString) {
this.warningMessages.push(warningString);
this.updateErrorView();
}
setRegionRootElement(rootElement) {
this.errorParentElement = rootElement.createDiv({
cls: `${MultiColumnLayoutCSS.RegionErrorContainerDiv}`,
});
this.titleRegion = this.errorParentElement.createDiv({
cls: `${MultiColumnLayoutCSS.ErrorRegionPadding}`
});
this.contentEl = this.errorParentElement.createDiv({
cls: `${MultiColumnLayoutCSS.ErrorRegionPadding} mcm-message-region`
});
this.updateErrorView();
}
setupErrorHeader() {
if (this.errorMessages.length > 0) {
let text = "Error";
if (this.errorMessages.length > 1) {
text = text + "s";
}
this.titleRegion.createSpan({
attr: { "style": "color: var(--text-error); padding: 5px;" },
text: "\u2A02"
});
this.titleRegion.createSpan({
text: `${this.errorMessages.length} ${text}`
});
}
if (this.errorMessages.length > 0 && this.warningMessages.length > 0) {
this.titleRegion.createSpan({
text: ` and `
});
}
if (this.warningMessages.length > 0) {
let text = "Warning";
if (this.warningMessages.length > 1) {
text = text + "s";
}
this.titleRegion.createSpan({
attr: { "style": "color: var(--color-yellow); padding: 5px;" },
text: "\u26A0"
});
this.titleRegion.createSpan({
text: `${this.warningMessages.length} ${text}`
});
}
this.titleRegion.createSpan({
text: ` in region`
});
let regionOpened = false;
this.titleRegion.addEventListener("click", (ev) => {
this.titleRegion.classList.toggle("mcm-error-heading-open");
regionOpened = !regionOpened;
if (regionOpened) {
this.contentEl.style.maxHeight = this.contentEl.scrollHeight + "px";
}
else {
this.contentEl.style.maxHeight = null;
}
});
}
updateErrorView() {
this.resetErrorView();
if (this.totalNumMessages === 0) {
return;
}
this.titleRegion.addClass(`mcm-error-heading`); //TODO: move to const.
this.errorParentElement.classList.add(MultiColumnStyleCSS.ColumnBorder);
this.setupErrorHeader();
this.appendContentToEl();
}
appendContentToEl() {
for (let i = 0; i < this.errorMessages.length; i++) {
let p = this.contentEl.createEl("p");
p.innerHTML = `<span class="mcm-error-icon">\u2A02</span>${this.errorMessages[0]}`;
}
for (let i = 0; i < this.warningMessages.length; i++) {
let p = this.contentEl.createEl("p");
p.innerHTML = `<span class="mcm-warning-icon">\u26A0</span>${this.warningMessages[0]}`;
}
}
resetErrorView() {
var _a;
this.errorParentElement.classList.remove(MultiColumnStyleCSS.ColumnBorder);
this.titleRegion.removeClass(`mcm-error-heading`); //TODO: move to const.
while (this.titleRegion.children.length > 0) {
this.titleRegion.childNodes.forEach(child => {
this.titleRegion.removeChild(child);
});
}
if (this.contentEl === null) {
return;
}
let children = (_a = this.contentEl) === null || _a === void 0 ? void 0 : _a.childNodes;
children.forEach(child => {
if (child !== null && child.parentElement === this.contentEl) {
this.contentEl.removeChild(child);
}
});
}
}
function createColBreakWarning(type, errorManager) {
let typeErrorStr = "";
if (type === ElementColumnBreakType.preBreak) {
typeErrorStr = "at the begining of another element";
}
if (type === ElementColumnBreakType.postBreak) {
typeErrorStr = "at the end of another element";
}
if (type === ElementColumnBreakType.midBreak) {
typeErrorStr = "in the middle of two elements";
}
errorManager.addWarningMessage(`Detected a column break tag ${typeErrorStr}. Please make sure to surround column breaks with empty lines on both sides, or render issues may occur.`);
}
function parseColBreakErrorType(elementInfo, errorManager) {
if (elementInfo.objectTag !== DOMObjectTag.columnBreak &&
elementInfo.colBreakType === ElementColumnBreakType.none) {
return;
}
let errorType = elementInfo.colBreakType;
if (elementInfo.objectTag === DOMObjectTag.columnBreak) {
if (elementInfo.lineAbove === "" &&
elementInfo.lineBelow === "") {
return;
}
let lineAbove = elementInfo.lineAbove;
let lineBelow = elementInfo.lineBelow;
if (lineAbove !== "" && lineBelow === "") {
errorType = ElementColumnBreakType.postBreak;
}
if (lineAbove === "" && lineBelow !== "") {
errorType = ElementColumnBreakType.preBreak;
}
if (lineAbove !== "" && lineBelow !== "") {
errorType = ElementColumnBreakType.midBreak;
}
}
createColBreakWarning(errorType, errorManager);
}
function getLeafFromFilePath(workspace, filePath) {
function checkState(state) {
if (state["type"] === undefined ||
state["type"] !== "markdown") {
return false;
}
if (state["state"] === undefined) {
return false;
}
if (state["state"]["file"] === undefined) {
return false;
}
let stateFilePath = state["state"]["file"];
return stateFilePath === filePath;
}
if (workspace === null) {
return null;
}
let entries = Object.entries(workspace.getLayout());
let items = Array.from(entries).map((val) => {
return val[1];
});
while (items.length > 0) {
let entryObj = items.shift();
if (entryObj["id"] !== undefined && entryObj["type"] !== undefined) {
if (entryObj["type"] === "split" ||
entryObj["type"] === "tabs") {
items = items.concat(entryObj['children']);
continue;
}
if (entryObj["type"] === "leaf" &&
entryObj["id"] !== undefined &&
entryObj["state"] !== undefined) {
let id = entryObj["id"];
let state = entryObj["state"];
console.log(state);
let valid = checkState(state);
if (valid) {
return workspace.getLeafById(id);
}
}
}
}
return null;
}
function getPreviewLeafFromFilePath(workspace, filePath) {
function checkState(state) {
if (state["type"] === undefined ||
state["type"] !== "markdown") {
return false;
}
if (state["state"] === undefined) {
return false;
}
if (state["state"]["file"] === undefined) {
return false;
}
let stateFilePath = state["state"]["file"];
return stateFilePath === filePath;
}
let entries = Object.entries(workspace.getLayout());
let items = Array.from(entries).map((val) => {
return val[1];
});
while (items.length > 0) {
let entryObj = items.shift();
if (entryObj["id"] !== undefined && entryObj["type"] !== undefined) {
if (entryObj["type"] === "split" ||
entryObj["type"] === "tabs") {
items = items.concat(entryObj['children']);
continue;
}
if (entryObj["type"] === "leaf" &&
entryObj["id"] !== undefined &&
entryObj["state"] !== undefined) {
let id = entryObj["id"];
let state = entryObj["state"];
let isPreview = false;
if (state["state"] !== undefined &&
state["state"]["mode"] !== undefined) {
let mode = state["state"]["mode"];
isPreview = mode === "preview";
}
let validFilePath = checkState(state);
if (validFilePath && isPreview) {
return workspace.getLeafById(id);
}
}
}
}
return null;
}
/*
* Filename: multi-column-markdown/src/live_preview/MultiColumnMarkdown_Widget.ts
* Created Date: Tuesday, August 16th 2022, 4:38:43 pm
* Author: Cameron Robinson
*
* Copyright (c) 2022 Cameron Robinson
*/
const CACHE_MAX_DELTA_TIME_MS = 2 * 60 * 1000; // 2m
let livePreviewElementCache = new Map();
function clearCache(skipKey = "") {
return __awaiter(this, void 0, void 0, function* () {
let keys = Array.from(livePreviewElementCache.keys());
for (let key of keys) {
if (key === skipKey) {
// console.debug(`Element: ${index} | Skipping key: ${key.split(" : ")[0]}`)
continue;
}
if (livePreviewElementCache.has(key) === false) {
continue;
}
let val = livePreviewElementCache.get(key);
let deltaTimeMS = Date.now() - val.timestamp;
if ((val.element.parentNode === null || val.element.parentNode.parentNode === null) && deltaTimeMS > CACHE_MAX_DELTA_TIME_MS) {
// console.debug(`cache delta: ${deltaTimeMS} > ${CACHE_MAX_DELTA_TIME_MS} or 2 minutes.`)
livePreviewElementCache.delete(key);
}
else if (val.element.parentNode == null || val.element.parentNode.parentNode === null) ;
}
});
}
function clearLivePreviewCache() {
clearCache();
}
class MultiColumnMarkdown_LivePreview_Widget extends view.WidgetType {
constructor(originalText, contentData, userSettings, sourceFile, settingsText = "", regionType) {
super();
this.domList = [];
this.regionSettings = getDefaultMultiColumnSettings();
this.sourcePath = "";
this.contentData = contentData;
this.settingsText = settingsText;
this.sourceFile = sourceFile;
if (this.sourceFile === null) {
return;
}
this.elementCacheID = `${this.sourceFile.path} : ${this.contentData}`;
if (this.sourceFile) {
this.sourcePath = sourceFile.path;
}
if (userSettings !== null) {
this.regionSettings = userSettings;
}
let errorManager = new RegionErrorManager(createDiv());
if (regionType === "CODEBLOCK") {
errorManager.addErrorMessage("The codeblock region start syntax has been depricated. Please manually update to the current syntax defined in the ReadMe, run the \"Fix Multi-Column Syntax in Current File\" from the Command Palette, or use the \"Update Depricated Syntax\" command found in the plugin settings window. You must reload the file for changes to take effect.");
}
(() => __awaiter(this, void 0, void 0, function* () {
function hasBadContentBetween(contentBetween) {
let regexResult = new RegExp("((?: *\n){6,})").exec(contentBetween);
if (regexResult !== null) {
// console.log("Found at least 6 empty lines.")
return false;
}
regexResult = /^(?:(?! *$)(?! *--- *$)).+$/mg.exec(contentBetween);
if (regexResult !== null) {
return false;
}
return true;
}
let fileText = yield sourceFile.vault.cachedRead(sourceFile);
fileText = fileText.replace(originalText, "###--START_HERE--###");
let regexResult = new RegExp("###--START_HERE--###").exec(fileText);
if (regexResult === null) {
return;
}
fileText = fileText.slice(0, regexResult.index);
let nextStartTag = findEndTagClosestToEnd(fileText);
if (nextStartTag.found === false) {
return;
}
let errorString = hasBadContentBetween(fileText.slice(nextStartTag.endPosition));
if (errorString === false) {
return;
}
errorManager.addWarningMessage("Detected possible issue with the content between this region and the region above. \
If you experience page jumping when clicking within this document, please make sure there are at least 6 blank \
lines or some form of text content between the two regions. This is a known issue that is under investigation. Sorry for the inconvenience.");
}))();
if (livePreviewElementCache.has(this.elementCacheID)) {
let cache = livePreviewElementCache.get(this.elementCacheID);
let regionManager = cache.regionManager;
let regionSettingsEqual = MCSettings_isEqual(userSettings, cache.cacheSettings);
let pluginSettingsUpdated = MCM_SettingsManager.shared.lastUpdateTimestamp > cache.pluginSettingsUpdateTimestamp;
if (regionManager && regionSettingsEqual === true && pluginSettingsUpdated === false) {
regionManager.updateErrorManager(errorManager, cache.errorRootEl);
let useLivePreviewCache = MCM_SettingsManager.shared.useLivePreviewCache;
let fileLeaf = getPreviewLeafFromFilePath(app.workspace, this.sourceFile.path);
if (useLivePreviewCache && fileLeaf === null) {
return;
}
}
else {
livePreviewElementCache.delete(this.elementCacheID);
}
}
// Render the markdown content to our temp parent element.
this.tempParent = createDiv();
let elementMarkdownRenderer = new obsidian.MarkdownRenderChild(this.tempParent);
obsidian.MarkdownRenderer.renderMarkdown(this.contentData, this.tempParent, this.sourcePath, elementMarkdownRenderer);
let previousText = "";
let workingText = originalText;
// take all elements, in order, and create our DOM list.
let arr = Array.from(this.tempParent.children);
for (let i = 0; i < arr.length; i++) {
let el = this.fixElementRender(arr[i]);
let domObject = new DOMObject(el, [""]);
this.domList.push(domObject);
let newData = sliceWorkingTextToEl(domObject, previousText, workingText);
previousText = newData.previousText;
workingText = newData.workingText;
newData = attemptToFixCheckboxes(domObject, previousText, workingText, sourceFile, this.elementCacheID);
previousText = newData.previousText;
workingText = newData.workingText;
workingText = checkForColumnBreakErrors(domObject, previousText, workingText, errorManager);
}
// Set up the region manager data before then creating our region manager.
let regionData = {
domList: this.domList,
domObjectMap: new Map(),
regionParent: createDiv(),
fileManager: null,
regionalSettings: this.regionSettings,
regionKey: getUID(),
rootElement: createDiv(),
errorManager: errorManager
};
// Finally setup the type of region manager required.
if (this.regionSettings.numberOfColumns === 1) {
this.regionSettings = parseSingleColumnSettings(this.settingsText, this.regionSettings);
this.regionManager = new SingleColumnRegionManager(regionData);
}
else if (this.regionSettings.autoLayout === true) {
this.regionManager = new AutoLayoutRegionManager(regionData, true);
}
else {
this.regionManager = new StandardMultiColumnRegionManager(regionData);
}
clearCache(this.elementCacheID);
}
fixElementRender(el) {
let fixedEl = fixImageRender(el, this.sourcePath);
fixedEl = fixPDFRender(fixedEl, this.sourcePath);
fixedEl = fixFileEmbed(fixedEl, this.sourcePath);
fixedEl = fixTableRender(fixedEl);
fixedEl = fixUnSupportedRender(fixedEl);
return fixedEl;
}
toDOM() {
let useLivePreviewCache = MCM_SettingsManager.shared.useLivePreviewCache;
let fileLeaf = getPreviewLeafFromFilePath(app.workspace, this.sourceFile.path);
if (useLivePreviewCache &&
livePreviewElementCache.has(this.elementCacheID) &&
fileLeaf === null) {
return livePreviewElementCache.get(this.elementCacheID).element;
}
// Create our element to hold all of the live preview elements.
let el = document.createElement("div");
el.className = "mcm-cm-preview";
/**
* For situations where we need to know the rendered height, AutoLayout,
* the element must be rendered onto the screen to get the info, even if
* only for a moment. Here we attempt to get a leaf from the app so we
* can briefly append our element, check any data if required, and then
* remove it.
*/
let autolayoutLeaf = null;
if (app) {
let leaves = app.workspace.getLeavesOfType("markdown");
if (leaves.length > 0) {
autolayoutLeaf = leaves[0];
}
}
if (this.regionManager) {
this.errorRootEl = el.createDiv();
let contentElement = el.createDiv();
this.regionManager.getRegionData().errorManager.setRegionRootElement(this.errorRootEl);
let requireUnload = false;
if (autolayoutLeaf && this.regionManager instanceof AutoLayoutRegionManager) {
autolayoutLeaf.view.containerEl.appendChild(el);
requireUnload = true;
}
this.regionManager.renderRegionElementsToLivePreview(contentElement);
for (let domObj of this.regionManager.getRegionData().domList) {
fixListCSS(domObj.originalElement);
}
if (requireUnload) {
autolayoutLeaf.view.containerEl.removeChild(el);
}
}
fixExternalLinks(el);
livePreviewElementCache.set(this.elementCacheID, {
timestamp: Date.now(),
element: el,
regionManager: this.regionManager,
errorRootEl: this.errorRootEl,
cacheSettings: this.regionSettings,
pluginSettingsUpdateTimestamp: MCM_SettingsManager.shared.lastUpdateTimestamp
});
return el;
}
fixElementCSS(domObject) {
fixListCSS(domObject.originalElement);
}
}
const OBSIDIAN_LIVEPREVIEW_TABLE_CLASSES = "cm-embed-block markdown-rendered cm-table-widget show-indentation-guide";
function fixTableRender(el) {
if (el.tagName !== "TABLE") {
return el;
}
let parentDiv = createDiv({
"cls": OBSIDIAN_LIVEPREVIEW_TABLE_CLASSES
});
parentDiv.appendChild(el);
return parentDiv;
}
function fixFileEmbed(el, source) {
let embed = getEmbed(el);
if (embed === null) {
return el;
}
let alt = embed.getAttr("alt");
let src = embed.getAttr("src");
if (src === null) {
return el;
}
let file = app.metadataCache.getFirstLinkpathDest(src, source);
if (file === null) {
return el;
}
if (isMDExtension(file.extension) === false) {
return el;
}
return createLPErrorElement("File embeds are not supported in Live Preview.\nPlease use reading mode to view.", alt, src);
}
function createLPErrorElement(errorText, alt = "", src = "") {
let errorEl = createDiv({
cls: "internal-embed markdown-embed inline-embed is-loaded",
attr: {
"tabindex": "-1",
"contenteditable": "false"
}
});
errorEl.setAttr("alt", alt);
errorEl.setAttr("src", `app://obsidian.md/${src}`);
errorEl.appendChild(createDiv({
"cls": "embed-title markdown-embed-title",
}));
let contentEl = errorEl.createDiv({
"cls": `markdown-embed-content`,
});
let paragraph = contentEl.createEl("p", {
"cls": `${MultiColumnStyleCSS.RegionErrorMessage}, ${MultiColumnStyleCSS.SmallFont}`
});
paragraph.innerText = errorText;
return errorEl;
}
function fixPDFRender(el, source) {
let embed = getEmbed(el);
if (embed === null) {
return el;
}
let alt = embed.getAttr("alt");
let src = embed.getAttr("src");
if (src === null) {
return el;
}
let file = app.metadataCache.getFirstLinkpathDest(src, source);
if (file === null) {
return el;
}
if (isPDFExtension(file.extension) === false) {
return el;
}
return createLPErrorElement("Due to an update to Obsidian's PDF viewer, PDF embeds are currently not supported.\nSorry for the inconvienence.", alt, src);
}
function fixImageRender(el, source) {
let embed = getEmbed(el);
if (embed === null) {
return el;
}
let customWidth = embed.attributes.getNamedItem("width");
let alt = embed.getAttr("alt");
let src = embed.getAttr("src");
if (src === null) {
return el;
}
let file = app.metadataCache.getFirstLinkpathDest(src, source);
if (file === null) {
return el;
}
// If the link source is not an image we dont want to make any adjustments.
if (isImageExtension(file.extension) === false) {
return el;
}
let fixedEl = createDiv({
cls: "internal-embed image-embed is-loaded",
});
fixedEl.setAttr("alt", alt);
let resourcePath = app.vault.getResourcePath(file);
let image = fixedEl.createEl("img");
image.setAttr("src", resourcePath);
if (customWidth !== null) {
image.setAttr("width", customWidth.value);
}
return fixedEl;
}
function fixExternalLinks(el) {
let items = el.getElementsByClassName("external-link");
for (let linkEl of Array.from(items)) {
let link = linkEl;
if (link === undefined ||
link === null) {
continue;
}
// Remove the href from the link and setup an event listener to open the link in the default browser.
let href = link.getAttr("href");
link.removeAttribute("href");
link.addEventListener("click", (ev) => {
window.open(href);
});
}
items = el.getElementsByClassName("internal-link");
for (let linkEl of Array.from(items)) {
let link = linkEl;
if (link === undefined ||
link === null) {
continue;
}
// Removing the href from internal links is all that seems to be required to fix the onclick.
link.removeAttribute("href");
}
return el;
}
function getEmbed(el) {
// embeds can either be a <div class="internal-embed" or <p><div class="internal-embed"
// depending on the syntax this additional check is to fix false negatives when embed is
// the first case.
if (el.hasClass("internal-embed")) {
return el;
}
else {
let items = el.getElementsByClassName("internal-embed");
if (items.length === 1) {
return items[0];
}
}
return null;
}
function isImageExtension(extension) {
extension = extension.toLowerCase();
switch (extension) {
case "png":
case "jpg":
case "jpeg":
case "gif":
case "bmp":
case "svg":
case "webp":
return true;
}
return false;
}
function isPDFExtension(extension) {
return extension.toLowerCase() === "pdf";
}
function isMDExtension(extension) {
return extension.toLowerCase() === "md";
}
function fixListCSS(el) {
var _a;
if (el.tagName !== "UL" && el.tagName !== "OL") {
return el;
}
(_a = el.parentElement) === null || _a === void 0 ? void 0 : _a.addClass(ObsidianStyleCSS.RenderedMarkdown);
return el;
}
function fixUnSupportedRender(el) {
if (isTasksPlugin(el)) {
if (MCM_SettingsManager.shared.renderInlineElErrors === true) {
let fixedEl = createDiv();
let paragraph = fixedEl.createEl("p", {
"cls": `${MultiColumnStyleCSS.RegionErrorMessage} ${MultiColumnStyleCSS.SmallFont}`
});
paragraph.innerText = "The Tasks plugin is not supported in Live Preview.\nPlease use reading mode.";
return fixedEl;
}
return el;
}
return el;
}
function checkForColumnBreakErrors(domObject, previousText, workingText, errorManager) {
if (domObject.tag !== DOMObjectTag.columnBreak &&
domObject.elementIsColumnBreak === ElementColumnBreakType.none) {
return workingText;
}
let prevLine = previousText.split("\n").slice(-2).join("\n");
let checkText = workingText;
if (checkForParagraphInnerColEndTag(prevLine)) {
checkText = prevLine + workingText;
}
let nextColBreak = checkForParagraphInnerColEndTag(checkText);
if (nextColBreak === null) {
console.error("Error. Something went wrong parsing column break out of text.");
return workingText;
}
let startIndex = nextColBreak.index;
let matchLength = nextColBreak[0].length;
let endIndex = startIndex + matchLength;
let matchText = nextColBreak[0].trim();
let newWorkingText = checkText.slice(endIndex);
// Already parsed column break warning.
if (domObject.elementIsColumnBreak !== ElementColumnBreakType.none) {
parseColBreakErrorType({
lineAbove: "",
lineBelow: "",
objectTag: DOMObjectTag.none,
colBreakType: domObject.elementIsColumnBreak
}, errorManager);
return newWorkingText;
}
// Now we have a standard column break but due to changes in obsidian parsing may still
// require displaying an error message.
let endTagText = domObject.originalElement.innerText;
// make sure the element text is a column break just to be sure. This really should never fail.
if (containsColEndTag(endTagText) === false) {
// If something went wrong here we can not proceed with the next regex unless this passes.
console.error("Error parsing column-break tag back out of element text.", endTagText);
return workingText;
}
// make sure the text of the element matche the syntax of what we parsed from the text.
if (matchText !== endTagText) {
console.error("Error matching next col-break to current element. Can not continue.");
return workingText;
}
// Slice out the 20 characters before and after the column break and then get just
// the one line before and after to check if error message required.
let startIndexOffset = Math.clamp(startIndex - 20, 0, startIndex);
let endIndexOffset = Math.clamp(endIndex + 20, endIndex, checkText.length - 1);
let additionalText = checkText.slice(startIndexOffset, endIndexOffset);
let textBefore = additionalText.slice(0, 20);
let textAfter = additionalText.slice(20 + matchLength);
textBefore = textBefore.replace(endTagText, "");
let linesAbove = textBefore.split("\n").filter((val) => {
return val !== "";
});
let linesBelow = textAfter.split("\n").filter((val) => {
return val !== "";
});
if (linesAbove.length === 0 && linesBelow.length === 0) {
return workingText;
}
let lineAbove = linesAbove.last();
let lineBelow = linesBelow.first();
parseColBreakErrorType({
lineAbove: lineAbove,
lineBelow: lineBelow,
objectTag: DOMObjectTag.columnBreak,
colBreakType: ElementColumnBreakType.none
}, errorManager);
return newWorkingText;
}
function sliceWorkingTextToEl(domObject, previousText, workingText) {
function processParagraph() {
let regex = new RegExp(`^ *${escapeRegExp(domObject.originalElement.textContent)} *$`, "m");
let result = regex.exec(workingText);
if (result) {
let updatedPrevious = previousText + workingText.slice(0, result.index);
workingText.slice(result.index, result.index + result[0].length);
let updatedWorkingText = workingText.slice(result.index);
return {
previousText: updatedPrevious,
workingText: updatedWorkingText
};
}
return {
previousText: previousText,
workingText: workingText
};
}
function processHeader() {
let count = parseInt(domObject.originalElement.tagName.slice(1));
let text = '#'.repeat(count);
let regex = new RegExp(`^${text} +${escapeRegExp(domObject.originalElement.textContent)} *$`, "m");
let result = regex.exec(workingText);
if (result) {
let updatedPrevious = previousText + workingText.slice(0, result.index);
workingText.slice(result.index, result.index + result[0].length);
let updatedWorkingText = workingText.slice(result.index);
return {
previousText: updatedPrevious,
workingText: updatedWorkingText
};
}
return {
previousText: previousText,
workingText: workingText
};
}
if (domObject.originalElement.tagName === "P") {
return processParagraph();
}
if (domObject.originalElement.tagName === "H1" ||
domObject.originalElement.tagName === "H2" ||
domObject.originalElement.tagName === "H3" ||
domObject.originalElement.tagName === "H4" ||
domObject.originalElement.tagName === "H5") {
return processHeader();
}
return {
previousText: previousText,
workingText: workingText
};
}
function attemptToFixCheckboxes(domObject, textBeforeElement, textOfElementAndAfter, sourceFile, cacheID) {
if (domObject.originalElement.tagName !== "UL") {
return {
previousText: textBeforeElement,
workingText: textOfElementAndAfter
};
}
if (domObject.originalElement.hasClass("contains-task-list") === false) {
return {
previousText: textBeforeElement,
workingText: textOfElementAndAfter
};
}
let listItems = Array.from(domObject.originalElement.getElementsByTagName("li")).filter((item) => {
return item.hasClass("task-list-item");
});
let workingTextBefore = textBeforeElement;
let workingText = textOfElementAndAfter;
for (let listElement of listItems) {
let possibleCheckbox = listElement.getElementsByTagName("input");
if (possibleCheckbox.length !== 1) {
console.error("Error: Could not get input for task item.");
continue;
}
let checkbox = possibleCheckbox[0];
if (checkbox.getAttr("type") !== "checkbox") {
console.error("Error: Checkbox not of proper type");
continue;
}
if (checkbox.hasClass("task-list-item-checkbox") === false) {
console.error("Error: Checkbox is missing proper class.");
continue;
}
if (checkbox.onclick !== null) {
console.error("Error: OnClick aready defined, not overwriting method.");
continue;
}
let checkboxIsChecked = listElement.getAttr("data-task") === "x" || listElement.getAttr("data-task") === "X";
let checkboxTextRegexSearch = RegExp(`^( *)-( +)\\[${checkboxIsChecked ? "[xX]" : " *"}\\]( +)${escapeRegExp(listElement.innerText)}( *)$`, "m");
let checkboxTextRegexResult = checkboxTextRegexSearch.exec(workingText);
if (checkboxTextRegexResult === null) {
console.error("Could not find text in provided document.");
continue;
}
let startOfElementIndex = checkboxTextRegexResult.index;
let endOfElementIndex = startOfElementIndex + checkboxTextRegexResult[0].length;
let newTextAfter = workingText.slice(endOfElementIndex);
let onClickNewTextAfter = newTextAfter;
workingTextBefore = workingTextBefore + workingText.slice(0, startOfElementIndex);
let onClickNewTextBefore = workingTextBefore;
workingText = workingText.slice(startOfElementIndex);
let onClickNewWorkingText = workingText;
let spaceBeforeDash = checkboxTextRegexResult[1];
let spaceAfterDash = checkboxTextRegexResult[2];
let spaceAfterCheck = checkboxTextRegexResult[3];
let spaceAfterContent = checkboxTextRegexResult[4];
let currentCacheID = cacheID;
let sourceFilePath = sourceFile.path;
checkbox.onclick = () => {
let replacementLine = `${spaceBeforeDash}-${spaceAfterDash}[${checkboxIsChecked ? " " : "X"}]${spaceAfterCheck}${listElement.innerText}${spaceAfterContent}`;
let originalTextToReplace = onClickNewTextBefore + onClickNewWorkingText;
let newReplacementText = onClickNewTextBefore + replacementLine + onClickNewTextAfter;
if (livePreviewElementCache.has(currentCacheID)) {
let newCacheID = `${sourceFilePath} : ${newReplacementText}`;
let currentData = livePreviewElementCache.get(currentCacheID);
livePreviewElementCache.set(newCacheID, currentData);
currentCacheID = newCacheID;
}
(() => __awaiter(this, void 0, void 0, function* () {
let fileText = yield sourceFile.vault.read(sourceFile);
if (fileText.contains(originalTextToReplace) === false) {
console.error("Could not update file. File does not contain region text.");
return;
}
let newFileText = fileText.replace(originalTextToReplace, newReplacementText);
sourceFile.vault.modify(sourceFile, newFileText);
}))();
listElement.classList.toggle("is-checked");
if (checkboxIsChecked) {
listElement.removeAttribute("data-task");
}
else {
listElement.setAttribute("data-task", "x");
}
checkboxIsChecked = !checkboxIsChecked;
};
}
return {
previousText: workingTextBefore,
workingText: workingText
};
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/*
* Filename: multi-column-markdown/src/live_preview/cm6_livePreview.ts
* Created Date: Monday, August 1st 2022, 1:51:16 pm
* Author: Cameron Robinson
*
* Copyright (c) 2022 Cameron Robinson
*/
const multiColumnMarkdown_StateField = state.StateField.define({
create(state) {
return view.Decoration.none;
},
update(oldState, transaction) {
const builder = new state.RangeSetBuilder();
let ignoreFurtherIterations = false;
// Check if view is in live preview state.
if (transaction.state.field(obsidian.editorLivePreviewField) === false) {
return builder.finish();
}
/**
* When we have the while file we then get the entire doc text and check if it
* contains a MCM region so we know to break or not.
*/
let docLength = transaction.state.doc.length;
let docText = transaction.state.doc.sliceString(0, docLength);
if (containsRegionStart(docText) === false) {
return builder.finish();
}
language.syntaxTree(transaction.state).iterate({
enter(node) {
// If we find that the file does not contain any MCM regions we can flip this
// flag and skip all other node iterations, potentially saving a lot of compute time.
//
// We only want to run the generation once per state change. If
// a previous node has sucessfully generated regions we ignore all
// other nodes in the state.
if (ignoreFurtherIterations === true) {
return;
}
// We want to run on the whole file so we dont just look for a single token.
const tokenProps = node.type.prop(language.tokenClassNodeProp);
if (tokenProps !== undefined) {
return;
}
// We want to know where the user's cursor is, it can be
// selecting multiple regions of text as well so we need to know
// all locations. Used to know if we should render region as text or as preview.
let ranges = getCursorLineLocations();
// Setup our loop to render the regions as MCM.
let workingFileText = docText;
let loopIndex = 0;
let startIndexOffset = 0;
while (true) {
let regionData = getNextRegion(workingFileText, startIndexOffset, docText);
if (regionData === null) {
break;
}
let elementText = regionData.regionText;
workingFileText = regionData.remainingText;
let startIndex = regionData.startIndex;
let endIndex = regionData.endIndex;
startIndexOffset = endIndex;
// Here we check if the cursor is in this specific region.
let cursorInRegion = checkCursorInRegion(startIndex, endIndex, ranges);
if (cursorInRegion === true) ;
else {
let foundSettings = getSettingsData(regionData);
let userSettings = null;
let settingsText = "";
let originalText = elementText;
if (foundSettings !== null) {
elementText = foundSettings.contentData;
userSettings = foundSettings.settings;
settingsText = foundSettings.settingsText;
}
const editorInfo = transaction.state.field(obsidian.editorInfoField);
// At this point if the cursor isnt in the region we pass the data to the
// element to be rendered.
builder.add(startIndex, endIndex, view.Decoration.replace({
widget: new MultiColumnMarkdown_LivePreview_Widget(originalText, elementText, userSettings, editorInfo.file, settingsText, regionData.regionType),
}));
}
ignoreFurtherIterations = true;
// Infinite loop protection.
loopIndex++;
if (loopIndex > 100) {
console.warn("Potential issue with rendering Multi-Column Markdown live preview regions. If problem persists please file a bug report with developer.");
break;
}
}
},
});
return builder.finish();
function getCursorLineLocations() {
let ranges = [];
if (transaction.state.selection.ranges) {
ranges = transaction.state.selection.ranges.filter((range) => {
return range.empty;
}).map((range) => {
let line = transaction.state.doc.lineAt(range.head);
`${line.number}:${range.head - line.from}`;
return {
line: line,
position: range.head
};
});
}
return ranges;
}
function valueIsInRange(value, minVal, maxVal, inclusive = true) {
if (inclusive === true && (value === minVal || value === maxVal)) {
return true;
}
if (minVal < value && value < maxVal) {
return true;
}
return false;
}
function checkCursorInRegion(startIndex, endIndex, ranges) {
for (let i = 0; i < ranges.length; i++) {
// TODO: Maybe look into limiting this to the second and second to last line
// of the region as clicking right at the top or bottom of the region
// swaps it to unrendered.
let range = ranges[i];
if (valueIsInRange(range.position, startIndex, endIndex) === true) {
return true;
}
}
if (transaction.state.selection) {
for (let i = 0; i < transaction.state.selection.ranges.length; i++) {
let range = transaction.state.selection.ranges[i];
// If either the start or end of the selection is within the
// region range we do not render live preview.
if (valueIsInRange(range.from, startIndex, endIndex) ||
valueIsInRange(range.to, startIndex, endIndex)) {
return true;
}
// // Or if the entire region is within the selection range
// we do not render the live preview.
if (valueIsInRange(startIndex, range.from, range.to) &&
valueIsInRange(endIndex, range.from, range.to)) {
return true;
}
}
}
return false;
}
},
provide(field) {
return view.EditorView.decorations.from(field);
},
});
function getNextRegion(workingFileText, startIndexOffset, wholeDoc) {
let region = findNextRegion(workingFileText);
if (region === null) {
return null;
}
if (region.dataType === "CODEBLOCK" || region.dataType === "ORIGINAL") {
// Search for the first end tag after a start block. (No recursive columns.)
let endTagData = findEndTag(workingFileText.slice(region.data.startPosition));
if (endTagData.found === false) {
return null;
}
/**
* For the region we found get the start and end position of the tags so we
* can slice it out of the document.
*/
let startIndex = startIndexOffset + region.data.startPosition;
let endIndex = startIndex + endTagData.startPosition + endTagData.matchLength; // Without the matchLength will leave the end tag on the screen.
// This text is the entire region data including the start and end tags.
let elementText = wholeDoc.slice(startIndex, endIndex);
workingFileText = wholeDoc.slice(endIndex);
/**
* Update our start offset and the working text of the file so our next
* iteration knows where we left off
*/
let data = {
regionType: region.dataType,
regionText: elementText,
remainingText: workingFileText,
startIndex: startIndex,
endIndex: endIndex
};
return data;
}
if (region.dataType === "PADOC") {
let pandocData = region.data;
let startIndex = startIndexOffset + pandocData.startPosition;
let endIndex = startIndexOffset + pandocData.endPosition;
workingFileText = wholeDoc.slice(endIndex);
let data = {
regionType: region.dataType,
regionText: pandocData.content,
remainingText: workingFileText,
startIndex: startIndex,
endIndex: endIndex,
columnCount: pandocData.columnCount,
userSettings: pandocData.userSettings
};
return data;
}
}
function findNextRegion(workingFileText) {
// If there are multiple kinds of start blocks, the old way of parsing would cause issues.
// Now search for both kinds and determine what to do after search.
let startTagData_codeblockStart = { dataType: "CODEBLOCK", data: findStartCodeblock(workingFileText) };
let startTagData_originalStart = { dataType: "ORIGINAL", data: findStartTag(workingFileText) };
let pandocData = { dataType: "PADOC", data: findPandoc(workingFileText) };
if (startTagData_codeblockStart.data.found === false &&
startTagData_originalStart.data.found === false &&
pandocData.data.found === false) {
return null;
}
let regionsFound = [startTagData_codeblockStart, startTagData_originalStart, pandocData].filter((val) => { return val.data.found === true; });
if (regionsFound.length > 1) {
let sorted = regionsFound.sort((a, b) => {
return a.data.startPosition - b.data.endPosition;
});
return sorted.first();
}
if (startTagData_codeblockStart.data.found === true) {
return startTagData_codeblockStart;
}
if (startTagData_originalStart.data.found === true) {
return startTagData_originalStart;
}
if (pandocData.data.found === true) {
return pandocData;
}
throw ("Unknown type found when parsing region.");
}
function getSettingsData(regionData) {
let contentData = regionData.regionText;
function parseCodeBlockSettings(settingsStartData) {
let settingsText = contentData.slice(settingsStartData.startPosition, settingsStartData.endPosition);
contentData = contentData.replace(settingsText, "");
let settings = parseColumnSettings(settingsText);
return {
settings: settings,
settingsText: settingsText,
contentData: contentData
};
}
if (regionData.regionType === "CODEBLOCK") {
let settingsStartData = findStartCodeblock(contentData);
if (settingsStartData.found === false) {
return null;
}
return parseCodeBlockSettings(settingsStartData);
}
if (regionData.regionType === "ORIGINAL") {
let settingsStartData = findSettingsCodeblock(contentData);
if (settingsStartData.found === false) {
return null;
}
return parseCodeBlockSettings(settingsStartData);
}
if (regionData.regionType === "PADOC") {
let pandocData = regionData;
return {
settings: parsePandocSettings(pandocData.userSettings, pandocData.columnCount),
settingsText: "",
contentData: regionData.regionText
};
}
}
/**
* File: /src/live_preview/cm6_livePreivew_onClickFix.ts *
* Created Date: Friday, March 24th 2023, 6:10 pm *
* Author: Cameron Robinson *
* *
* Copyright (c) 2023 Cameron Robinson *
*/
const MultiColumnMarkdown_OnClickFix = state.StateField.define({
create(state) {
return view.Decoration.none;
},
update(oldState, transaction) {
const builder = new state.RangeSetBuilder();
return builder.finish();
},
provide(field) {
return view.EditorView.decorations.from(field);
},
});
function updateAllSyntax(originalFileContent) {
let fileCount = 0;
let regionStartCount = 0;
let columnBreakCount = 0;
let columnEndCount = 0;
let fileUpdated = false;
let { updatedFileContent, numRegionsUpdated } = updateColumnCodeblockStartSyntax(originalFileContent);
if (numRegionsUpdated > 0) {
fileCount++;
fileUpdated = true;
regionStartCount += numRegionsUpdated;
}
let colStart = updateColumnStartSyntax(updatedFileContent);
if (colStart.numRegionsUpdated) {
if (fileUpdated === false) {
fileUpdated = true;
fileCount++;
}
updatedFileContent = colStart.updatedFileContent;
regionStartCount += colStart.numRegionsUpdated;
}
let colBreak = updateColumnBreakSyntax(updatedFileContent);
if (colBreak.numRegionsUpdated) {
if (fileUpdated === false) {
fileUpdated = true;
fileCount++;
}
updatedFileContent = colBreak.updatedFileContent;
columnBreakCount += colBreak.numRegionsUpdated;
}
let colEnd = updateColumnEndSyntax(updatedFileContent);
if (colEnd.numRegionsUpdated) {
if (fileUpdated === false) {
fileUpdated = true;
fileCount++;
}
updatedFileContent = colEnd.updatedFileContent;
columnEndCount += colEnd.numRegionsUpdated;
}
return {
fileCount: fileCount,
regionStartCount: regionStartCount,
columnBreakCount: columnBreakCount,
columnEndCount: columnEndCount,
updatedFileContent: updatedFileContent,
fileWasUpdated: updatedFileContent !== originalFileContent
};
}
const OLD_COL_END_SYNTAX_REGEX = /=== *(end-multi-column|multi-column-end)/g;
function updateColumnEndSyntax(originalFileContent) {
let matches = Array.from(originalFileContent.matchAll(OLD_COL_END_SYNTAX_REGEX));
let updatedFileContent = originalFileContent;
let offset = 0;
for (let match of matches) {
let startIndex = match.index + offset;
let matchLength = match[0].length;
let endIndex = startIndex + matchLength;
let columnEndSyntax = match[1];
let replacementText = `--- ${columnEndSyntax}`;
offset += replacementText.length - matchLength;
updatedFileContent = updatedFileContent.slice(0, startIndex) + replacementText + updatedFileContent.slice(endIndex);
console.groupCollapsed();
console.log("Original File:\n\n", originalFileContent);
console.log("Updated File:\n\n", updatedFileContent);
console.groupEnd();
}
return {
updatedFileContent: updatedFileContent,
numRegionsUpdated: matches.length
};
}
const OLD_COL_BREAK_SYNTAX_REGEX = /===\s*?(column-end|end-column|column-break|break-column)\s*?===\s*?/g;
function updateColumnBreakSyntax(originalFileContent) {
let matches = Array.from(originalFileContent.matchAll(OLD_COL_BREAK_SYNTAX_REGEX));
let updatedFileContent = originalFileContent;
let offset = 0;
for (let match of matches) {
let startIndex = match.index + offset;
let matchLength = match[0].length;
let endIndex = startIndex + matchLength;
let columnBreakSyntax = match[1];
let replacementText = `--- ${columnBreakSyntax} ---`;
offset += replacementText.length - matchLength;
updatedFileContent = updatedFileContent.slice(0, startIndex) + replacementText + updatedFileContent.slice(endIndex);
console.groupCollapsed();
console.log("Original File:\n\n", originalFileContent);
console.log("Updated File:\n\n", updatedFileContent);
console.groupEnd();
}
return {
updatedFileContent: updatedFileContent,
numRegionsUpdated: matches.length
};
}
const OLD_COL_START_SYNTAX_REGEX = /=== *(start-multi-column|multi-column-start)/g;
function updateColumnStartSyntax(originalFileContent) {
let matches = Array.from(originalFileContent.matchAll(OLD_COL_START_SYNTAX_REGEX));
let updatedFileContent = originalFileContent;
let offset = 0;
for (let match of matches) {
let startIndex = match.index + offset;
let matchLength = match[0].length;
let endIndex = startIndex + matchLength;
let columnStartSyntax = match[1];
let replacementText = `--- ${columnStartSyntax}`;
offset += replacementText.length - matchLength;
updatedFileContent = updatedFileContent.slice(0, startIndex) + replacementText + updatedFileContent.slice(endIndex);
console.groupCollapsed();
console.log("Original File:\n\n", originalFileContent);
console.log("Updated File:\n\n", updatedFileContent);
console.groupEnd();
}
return {
updatedFileContent: updatedFileContent,
numRegionsUpdated: matches.length
};
}
const OLD_CODEBLOCK_COL_START_SYNTAX_REGEX = /```(start-multi-column|multi-column-start).*?```/sg;
function updateColumnCodeblockStartSyntax(originalFileContent) {
let matches = Array.from(originalFileContent.matchAll(OLD_CODEBLOCK_COL_START_SYNTAX_REGEX));
let updatedFileContent = originalFileContent;
let offset = 0;
for (let match of matches) {
let startIndex = match.index + offset;
let matchLength = match[0].length;
let endIndex = startIndex + matchLength;
let originalSettingsText = match[0];
let settingsText = originalSettingsText;
let columnStartSyntax = match[1];
let columnStartLine = `--- ${columnStartSyntax}`;
let idResult = /ID:(.*)/i.exec(originalSettingsText);
if (idResult !== null) {
let id = idResult[1].trim();
columnStartLine = `${columnStartLine}: ${id}`;
let startIndex = idResult.index;
let endIndex = startIndex + idResult[0].length;
settingsText = originalSettingsText.slice(0, startIndex);
settingsText += originalSettingsText.slice(endIndex + 1);
}
settingsText = settingsText.replace(columnStartSyntax, "column-settings");
let replacementText = `${columnStartLine}\n${settingsText}`;
offset += replacementText.length - matchLength;
updatedFileContent = updatedFileContent.slice(0, startIndex) + replacementText + updatedFileContent.slice(endIndex);
console.groupCollapsed();
console.log("Original File:\n\n", originalFileContent);
console.log("Updated File:\n\n", updatedFileContent);
console.groupEnd();
}
return {
updatedFileContent: updatedFileContent,
numRegionsUpdated: matches.length
};
}
class MultiColumnSettingsView extends obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
this.containerEl.empty();
this.containerEl.createEl("h2", { text: "Multi-Column Markdown - Settings" });
const settingsContainerEl = this.containerEl.createDiv();
new obsidian.Setting(settingsContainerEl)
.setName("Number of auto-layout balance iterations")
.setDesc("The maximum number of times auto-layout will try to balance elements between all of the columns. Setting this too high may cause Obsidian to slow down during loading and refreshing of Auto-Layout columns.")
.addSlider((slider) => {
slider.setLimits(1, 15, 2);
slider.setValue(MCM_SettingsManager.shared.autoLayoutBalanceIterations);
slider.setDynamicTooltip();
slider.onChange((val) => {
MCM_SettingsManager.shared.autoLayoutBalanceIterations = val;
this.plugin.saveSettings();
});
});
new obsidian.Setting(settingsContainerEl)
.setName("Align tables with text alignment by default")
.setDesc(this.buildTableAlignDocFrag())
.addToggle((t) => t.setValue(MCM_SettingsManager.shared.alignTablesToContentAlignment)
.onChange((v) => {
MCM_SettingsManager.shared.alignTablesToContentAlignment = v;
this.plugin.saveSettings();
}));
new obsidian.Setting(settingsContainerEl)
.setName("Use Live Preview render cache")
.setDesc(this.buildRenderCacheDocFrag())
.addToggle((t) => t.setValue(MCM_SettingsManager.shared.useLivePreviewCache)
.onChange((v) => {
MCM_SettingsManager.shared.useLivePreviewCache = v;
this.plugin.saveSettings();
}));
if (obsidian.Platform.isMobile === true) {
new obsidian.Setting(settingsContainerEl)
.setName("Render column regions on mobile devices")
.addToggle((t) => t.setValue(MCM_SettingsManager.shared.renderOnMobile).onChange((v) => {
MCM_SettingsManager.shared.renderOnMobile = v;
this.plugin.saveSettings();
}));
}
this.containerEl.createEl("h5", { attr: { "style": "color: var(--text-error); margin-bottom: 0px;" }, text: "DANGER ZONE" });
this.containerEl.createEl("hr", { attr: { "style": "margin-top: 1px; margin-bottom: 0.75em;" } });
const dangerZoneContainerEl = this.containerEl.createDiv();
this.buildUpdateDepricated(dangerZoneContainerEl);
this.buildFixMissingIDs(dangerZoneContainerEl);
this.containerEl.createEl("br");
let { bgColor, fontColor, coffeeColor } = getDonateButtonColors(this.containerEl);
new obsidian.Setting(this.containerEl)
.setName("Donate")
.setDesc(`If you like this Plugin, please consider providing a one time donation to support it's development.`)
.addButton((b) => {
b.buttonEl.setAttr("style", "background-color: transparent; height: 30pt; padding: 0px;");
const div = b.buttonEl.createDiv({ attr: { "style": "width: 100%; height: 100%" } });
div.createEl("a", {
href: "https://www.buymeacoffee.com/ckrobinson"
}).createEl("img", {
attr: {
style: "width: 100%; height: 100%",
src: `https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=ckrobinson&button_colour=${bgColor}&font_colour=${fontColor}&font_family=Cookie&outline_colour=000000&coffee_colour=${coffeeColor}`
}
});
});
}
buildRenderCacheDocFrag() {
let docFrag = new DocumentFragment();
docFrag.createDiv({}, div => {
div.createSpan({}, span => {
span.innerText = "Caches rendered content in Live Preview to reduce render cycles and element flashing on note interaction.";
});
div.createEl("br");
div.createEl("br");
div.createSpan({}, span => {
span.innerText = "Only uses cache when a file Live Preview tab is open. If both reading view and Live Preview are opened this feature is disabled.";
});
div.createEl("br");
div.createEl("br");
div.createEl("h5", {}, span => {
span.setAttr("style", "color: var(--text-error); margin-bottom: 0px; margin-top: 3px;");
span.innerText = "EXPERIMENTAL:";
});
div.createSpan({}, span => {
span.setAttr("style", "color: var(--text-error);");
span.innerText = "This feature is experimental only and has intermittently caused notes to erase column content during development. A fix has been implemented \
but due to the potential data loss you must opt-in to using this feature. If content is erased you can use Undo to restore the file data. \
Please make backups and disable if you experience any data loss.";
});
});
return docFrag;
}
buildTableAlignDocFrag() {
let docFrag = new DocumentFragment();
docFrag.createDiv({}, div => {
div.createSpan({}, span => {
span.innerText = "Sets the defalut behavior when determining whether to align table to text alignment.";
});
div.createEl("br");
div.createEl("ul").createEl("li", { text: "This value is overwritten when defining the column setting: 'Align Tables to Text Alignment: true/false'" });
});
return docFrag;
}
buildUpdateDepricated(dangerZoneContainerEl) {
let docFrag = new DocumentFragment();
docFrag.createDiv({}, div => {
div.createSpan({}, span => {
span.innerText = "This may take a while for large vaults, you can continue to use Obsidian but application may slow down during process.";
});
div.createEl("br");
div.createEl("br");
div.createEl("h5", {}, span => {
span.setAttr("style", "color: var(--text-error); margin-bottom: 0px; margin-top: 3px;");
span.innerText = "WARNING:";
});
div.createSpan({}, span => {
span.setAttr("style", "color: var(--text-error);");
span.innerText = "This action modifies any note file with depricated syntax and could lead to corrupted file text.";
});
div.createEl("br");
div.createSpan({}, span => {
span.setAttr("style", "color: var(--text-error);");
span.innerText = "No guarentee is given. Please make sure to back your vault up first.";
});
});
let modalDescriptionEl = createDiv({}, div => {
div.createSpan({ text: "This action may corrupt vault data." });
div.createEl("br");
div.createSpan({ text: "Please confirm you have backed up your vault." });
});
new obsidian.Setting(dangerZoneContainerEl)
.setName("Update ALL depricated Multi-Column syntax.")
.setDesc(docFrag)
.addButton((b) => b.setButtonText("Update Syntax").onClick(() => {
const modal = ConfirmModal.confirmModalWithElement(this.app, modalDescriptionEl, { primary: "Confirm", secondary: "Cancel" });
modal.onClose = () => {
if (modal.confirmed === false) {
return;
}
updateFileSyntax();
};
modal.open();
}));
}
buildFixMissingIDs(dangerZoneContainerEl) {
let docFrag = new DocumentFragment();
docFrag.createDiv({}, div => {
div.createSpan({}, span => {
span.innerText = "This will only modify column regions without a pre-defined ID, and which use the up to date core syntax. Will not modify depricated syntax or fenced-divs.";
});
div.createEl("br");
div.createEl("br");
div.createSpan({}, span => {
span.innerText = "This may take a while for large vaults, you can continue to use Obsidian but application may slow down during process.";
});
div.createEl("br");
div.createEl("br");
div.createEl("h5", {}, span => {
span.setAttr("style", "color: var(--text-error); margin-bottom: 0px; margin-top: 3px;");
span.innerText = "WARNING:";
});
div.createSpan({}, span => {
span.setAttr("style", "color: var(--text-error);");
span.innerText = "This action modifies any note file missing column IDs and could lead to corrupted file text.";
});
div.createEl("br");
div.createSpan({}, span => {
span.setAttr("style", "color: var(--text-error);");
span.innerText = "No guarentee is given. Please make sure to back your vault up first.";
});
});
let modalDescriptionEl = createDiv({}, div => {
div.createSpan({ text: "This action may corrupt vault data." });
div.createEl("br");
div.createSpan({ text: "Please confirm you have backed up your vault." });
});
new obsidian.Setting(dangerZoneContainerEl)
.setName("Append IDs to all Multi-Column regions in vault.")
.setDesc(docFrag)
.addButton((b) => b.setButtonText("Add IDs").onClick(() => {
const modal = ConfirmModal.confirmModalWithElement(this.app, modalDescriptionEl, { primary: "Confirm", secondary: "Cancel" });
modal.onClose = () => {
if (modal.confirmed === false) {
return;
}
findAndReplaceMissingIDs();
};
modal.open();
}));
}
}
class ConfirmModal extends obsidian.Modal {
static confirmModalWithText(app, text, buttons) {
return new ConfirmModal(app, createSpan({ text: text }), buttons);
}
static confirmModalWithElement(app, descriptionEl, buttons) {
return new ConfirmModal(app, descriptionEl, buttons);
}
constructor(app, descriptionEl, buttons) {
super(app);
this.confirmed = false;
this.descriptionEl = descriptionEl;
this.buttons = buttons;
}
display() {
return __awaiter(this, void 0, void 0, function* () {
this.contentEl.empty();
this.contentEl.appendChild(this.descriptionEl);
const buttonEl = this.contentEl.createDiv();
new obsidian.ButtonComponent(buttonEl)
.setButtonText(this.buttons.primary)
.setCta()
.onClick(() => {
this.confirmed = true;
this.close();
});
new obsidian.ButtonComponent(buttonEl)
.setButtonText(this.buttons.secondary)
.onClick(() => {
this.close();
});
});
}
onOpen() {
this.display();
}
}
function findAndReplaceMissingIDs() {
return __awaiter(this, void 0, void 0, function* () {
function searchFileForMissingID(docText) {
let lines = docText.split("\n");
/**
* Loop through all of the lines checking if the line is a
* start tag and if so is it missing an ID.
*/
let linesWithoutIDs = 0;
for (let i = 0; i < lines.length; i++) {
let data = isStartTagWithID(lines[i]);
if (data.isStartTag === true && data.hasKey === false) {
let originalText = lines[i];
let text = originalText;
text = text.trimEnd();
if (text.charAt(text.length - 1) === ":") {
text = text.slice(0, text.length - 1);
}
text = `${text}: ID_${getUID(4)}`;
lines[i] = text;
linesWithoutIDs++;
}
}
if (linesWithoutIDs === 0) {
return {
updatedFileContent: docText,
numRegionsUpdated: 0
};
}
let newFileContent = lines.join("\n");
return {
updatedFileContent: newFileContent,
numRegionsUpdated: linesWithoutIDs
};
}
let count = 0;
let fileCount = 0;
for (let mdFile of app.vault.getMarkdownFiles()) {
let originalFileContent = yield app.vault.read(mdFile);
let result = searchFileForMissingID(originalFileContent);
if (result.numRegionsUpdated > 0) {
count += result.numRegionsUpdated;
fileCount++;
let updatedFileContent = result.updatedFileContent;
if (updatedFileContent !== originalFileContent) {
app.vault.modify(mdFile, updatedFileContent);
}
else {
console.log("No changes, not updating file.");
}
}
}
new obsidian.Notice(`Finished updating:\n${count} region IDs, across ${fileCount} files.`);
});
}
function updateFileSyntax() {
return __awaiter(this, void 0, void 0, function* () {
let fileCount = 0;
let regionStartCount = 0;
let columnBreakCount = 0;
let columnEndCount = 0;
for (let mdFile of app.vault.getMarkdownFiles()) {
let originalFileContent = yield app.vault.read(mdFile);
let result = updateAllSyntax(originalFileContent);
fileCount += result.fileCount;
regionStartCount += result.regionStartCount;
columnBreakCount += result.columnBreakCount;
columnEndCount += result.columnEndCount;
let updatedFileContent = result.updatedFileContent;
// TODO: Add in final file modification when done testing.
if (result.fileWasUpdated) {
app.vault.modify(mdFile, updatedFileContent);
}
}
new obsidian.Notice(`Finished updating:\n${regionStartCount} start syntaxes,\n${columnBreakCount} column breaks, and\n${columnEndCount} column end tags,\nacross ${fileCount} files.`);
});
}
function getDonateButtonColors(containerEl) {
let computedStyle = getComputedStyle(containerEl);
let fontColor = computedStyle.getPropertyValue('--text-normal');
let bgColor = computedStyle.getPropertyValue('--accent');
let coffeeColor = "ffffff";
if (isValidHexColor(fontColor) &&
isValidHexColor(bgColor)) {
fontColor = fontColor.slice(1);
bgColor = bgColor.slice(1);
coffeeColor = fontColor;
}
else {
fontColor = "000000";
bgColor = "FFDD00";
coffeeColor = "ffffff";
}
return { bgColor, fontColor, coffeeColor };
}
function isValidHexColor(possibleColor) {
let firstChar = possibleColor[0];
if (firstChar !== "#") {
return false;
}
if (possibleColor.length !== 7) {
return false;
}
return /^#[0-9A-F]{6}$/i.test(possibleColor);
}
/*
* File: multi-column-markdown/src/main.ts
* Created Date: Tuesday, October 5th 2021, 1:09 pm
* Author: Cameron Robinson
*
* Copyright (c) 2022 Cameron Robinson
*/
const CODEBLOCK_START_STRS = [
"start-multi-column",
"multi-column-start"
];
class MultiColumnMarkdown extends obsidian.Plugin {
constructor() {
super(...arguments);
this.settingsManager = MCM_SettingsManager.shared;
this.globalManager = new GlobalDOMManager();
//#endregion PDF Exporting.
}
onload() {
return __awaiter(this, void 0, void 0, function* () {
console.log("Loading multi-column markdown");
yield this.loadSettings();
this.globalManager = new GlobalDOMManager();
this.registerEditorExtension(multiColumnMarkdown_StateField);
this.registerEditorExtension(MultiColumnMarkdown_OnClickFix);
for (let i = 0; i < CODEBLOCK_START_STRS.length; i++) {
let startStr = CODEBLOCK_START_STRS[i];
this.setupMarkdownCodeblockPostProcessor(startStr);
}
this.setupMarkdownPostProcessor();
this.addSettingTab(new MultiColumnSettingsView(this.app, this));
this.addCommand({
id: `toggle-mobile-rendering-mcm`,
name: `Toggle Mobile Rendering - Multi-Column Markdown`,
callback: () => __awaiter(this, void 0, void 0, function* () {
this.settingsManager.renderOnMobile = !this.settingsManager.renderOnMobile;
yield this.saveSettings();
let noticeString = `Toggled mobile rendering ${this.settingsManager.renderOnMobile ? "on" : "off"}.`;
if (obsidian.Platform.isMobile === true) {
noticeString += ` Please reload any open files for change to take effect.`;
}
new obsidian.Notice(noticeString);
})
});
//TODO: Set up this as a modal to set settings automatically
this.addCommand({
id: `insert-multi-column-region`,
name: `Insert Multi-Column Region`,
editorCallback: (editor, view) => {
try {
let cursorStartPosition = editor.getCursor("from");
editor.getDoc().replaceSelection(`
--- start-multi-column: ID_${getUID(4)}
\`\`\`column-settings
Number of Columns: 2
Largest Column: standard
\`\`\`
--- column-break ---
--- end-multi-column
${editor.getDoc().getSelection()}`);
cursorStartPosition.line = cursorStartPosition.line + 7;
cursorStartPosition.ch = 0;
editor.setCursor(cursorStartPosition);
}
catch (e) {
new obsidian.Notice("Encountered an error inserting a multi-column region. Please try again later.");
}
}
});
this.addCommand({
id: `clear-lp-cache-mcm`,
name: `Clear Live Preview Cache - Multi-Column Markdown`,
callback: () => __awaiter(this, void 0, void 0, function* () {
clearLivePreviewCache();
})
});
this.addCommand({
id: `add-IDs-To-multi-column-region`,
name: `Fix Missing IDs for Multi-Column Regions`,
editorCallback: (editor, view) => {
try {
/**
* Not sure if there is an easier way to do this.
*
* Get all of the lines of the document split by newlines.
*/
let docText = editor.getRange({ line: 0, ch: 0 }, { line: editor.getDoc().lineCount(), ch: 0 });
let lines = docText.split("\n");
let startCodeblock = findStartCodeblock(docText);
let lineOffset = 0;
let numCodeblocksUpdated = 0;
while (startCodeblock.found === true) {
// Get the text of the settings block so we can check if it contains an ID,
// also so we can get the length of the first line, used to calculate where to append a new ID if needed
let settingsText = docText.slice(startCodeblock.startPosition, startCodeblock.endPosition);
let firstLineOfCodeblockLength = settingsText.split("\n")[0].length;
// We need the lines before the block to know where to start replacing text
// and the lines including the block to know where to set our offset to after this iteration.
let linesBefore = docText.slice(0, startCodeblock.startPosition);
let startReplacementLineIndex = (linesBefore.split("\n").length - 1) + lineOffset;
let linesOf = docText.slice(0, startCodeblock.endPosition);
let endReplacementLineIndex = (linesOf.split("\n").length - 1) + lineOffset;
let settingsID = parseStartRegionCodeBlockID(settingsText);
if (settingsID === "") {
// copy the first line of the codeblock and append a new ID, then replace the first line of the block
let replacementText = editor.getRange({ line: startReplacementLineIndex, ch: 0 }, { line: startReplacementLineIndex, ch: firstLineOfCodeblockLength }) + `\nID: ID_${getUID(4)}`;
editor.replaceRange(replacementText, { line: startReplacementLineIndex, ch: 0 }, { line: startReplacementLineIndex, ch: firstLineOfCodeblockLength });
endReplacementLineIndex += 1;
numCodeblocksUpdated += 1;
}
lineOffset = endReplacementLineIndex;
docText = docText.slice(startCodeblock.endPosition);
startCodeblock = findStartCodeblock(docText);
}
/**
* Loop through all of the lines checking if the line is a
* start tag and if so is it missing an ID.
*/
let linesWithoutIDs = [];
let textWithoutIDs = [];
for (let i = 0; i < lines.length; i++) {
let data = isStartTagWithID(lines[i]);
if (data.isStartTag === true && data.hasKey === false) {
linesWithoutIDs.push(i);
textWithoutIDs.push(lines[i]);
}
}
if (linesWithoutIDs.length === 0 && numCodeblocksUpdated === 0) {
new obsidian.Notice("Found 0 missing IDs in the current document.");
return;
}
/**
* Now loop through each line that is missing an ID and
* generate a random ID and replace the original text.
*/
for (let i = 0; i < linesWithoutIDs.length; i++) {
let originalText = textWithoutIDs[i];
let text = originalText;
text = text.trimEnd();
if (text.charAt(text.length - 1) === ":") {
text = text.slice(0, text.length - 1);
}
text = `${text}: ID_${getUID(4)}`;
editor.replaceRange(text, { line: linesWithoutIDs[i], ch: 0 }, { line: linesWithoutIDs[i], ch: originalText.length });
}
new obsidian.Notice(`Replaced ${linesWithoutIDs.length + numCodeblocksUpdated} missing ID(s) in the current document.`);
}
catch (e) {
new obsidian.Notice("Encountered an error addign IDs to multi-column regions. Please try again later.");
}
}
});
this.addCommand({
id: `mcm-Toggle-Document-Reflow`,
name: `Setup Multi-Column Reflow - Multi-Column Markdown`,
editorCallback: (editor, view) => {
app.fileManager.processFrontMatter(view.file, (frontmatter) => {
let isReflow = isMultiColumnReflow(frontmatter);
if (isReflow) {
return;
}
frontmatter["Multi-Column Markdown"] = [
{ "Number of Columns": 2 },
{ "Column Size": "Standard" }
];
view.editor.refresh();
});
}
});
this.addCommand({
id: `mcm-fix-file-multi-column-syntax`,
name: `Fix Multi-Column Syntax in Current File.`,
editorCallback: (editor, view) => {
try {
let fromPosition = { line: 0, ch: 0 };
let toPosition = { line: editor.getDoc().lineCount(), ch: 0 };
let docText = editor.getRange(fromPosition, toPosition);
let result = updateAllSyntax(docText);
let regionStartCount = result.regionStartCount;
let columnBreakCount = result.columnBreakCount;
let columnEndCount = result.columnEndCount;
let updatedFileContent = result.updatedFileContent;
if (result.fileWasUpdated) {
editor.replaceRange(updatedFileContent, fromPosition, toPosition);
new obsidian.Notice(`Finished updating:\n${regionStartCount} start syntaxes,\n${columnBreakCount} column breaks, and\n${columnEndCount} column end tags.`);
}
else {
new obsidian.Notice(`Found no region syntax to update.`);
}
}
catch (e) {
new obsidian.Notice("Encountered an error fixing multi-column region syntax. Please try again later.");
}
}
});
this.registerInterval(window.setInterval(() => {
this.UpdateOpenFilePreviews();
}, 500));
});
}
UpdateOpenFilePreviews() {
let fileManagers = this.globalManager.getAllFileManagers();
fileManagers.forEach(element => {
let regionalManagers = element.getAllRegionalManagers();
regionalManagers.forEach(regionManager => {
regionManager.updateRenderedMarkdown();
});
});
}
setupMarkdownPostProcessor() {
this.registerMarkdownPostProcessor((el, ctx) => __awaiter(this, void 0, void 0, function* () {
if (this.settingsManager.renderOnMobile === false &&
obsidian.Platform.isMobile === true) {
return;
}
const sourcePath = ctx.sourcePath;
let fileDOMManager = this.globalManager.getFileManager(sourcePath);
if (fileDOMManager === null) {
console.warn("Found null DOM manager. Could not process multi-column markdown.");
return;
}
/**
* Here we check if the export "print" flag is in the DOM so we can determine if we
* are exporting and handle that case.
*/
if (this.checkExporting(el)) {
this.exportDocumentToPDF(el, fileDOMManager, sourcePath);
}
// Get the info for our current context and then check
// if the entire text contains a start tag. If there is
// no start tag in the document we can just return and
// ignore the rest of the parsing.
let info = ctx.getSectionInfo(el);
/**
* We need the context info to properly parse so returning here
* info is null. TODO: Set error in view if this occurs.
*/
if (!info) {
return;
}
let docString = info.text;
let docLines = docString.split("\n");
let reflowFrontmatter = isMultiColumnReflow(ctx.frontmatter);
if (reflowFrontmatter === true) {
this.renderDocReflow(el, ctx, sourcePath, fileDOMManager, docString, info);
return;
}
else {
fileDOMManager.removeRegion("Multi-Column Reflow Region");
}
/**
* If we encounter a start tag on the document we set the flag to start
* parsing the rest of the document.
*/
if (containsRegionStart(docString)) {
fileDOMManager.setHasStartTag();
}
/**
* If the document does not contain any start tags we ignore the
* rest of the parsing. This is only set to true once the first
* start tag element is parsed above.
*/
if (fileDOMManager.getHasStartTag() === false) {
return;
}
/**
* Take the info provided and generate the required variables from
* the line start and end values.
*/
let relativeTexts = extractElementRelativeLocationData(docLines, info);
/**
* If the current line is a start tag we want to set up the
* region manager. The regional manager takes care
* of all items between it's start and end tags while the
* file manager we got above above takes care of all regional
* managers in each file.
*/
if (containsStartTag(relativeTexts.textOfElement)) {
createStartElement(el, relativeTexts.linesOfElement, ctx, fileDOMManager, docString);
return;
}
// Pandoc Start Region Tag.
if (containsPandocStartTag(relativeTexts.textOfElement)) {
createPandocStartElement(el, relativeTexts.textOfElement, ctx, fileDOMManager, docString);
return;
}
/**
* Check if any of the lines above us contain a start block, and if
* so get the lines from our current element to the start block.
*/
let startBockAbove = getStartDataAboveLine(relativeTexts.linesAboveArray);
if (startBockAbove === null) {
return;
}
/**
* We now know we're within a multi-column region, so we update our
* list of lines above to just be the items within this region.
*/
relativeTexts.linesAboveArray = startBockAbove.linesAboveArray;
/**
* We use the start block's key to get our regional manager. If this
* lookup fails we can not continue processing this element.
*/
let regionalContainer = fileDOMManager.getRegionalContainer(startBockAbove.startBlockKey);
if (regionalContainer === null) {
return;
}
regionalContainer.getRegion();
/**
* To make sure we're placing the item in the right location (and
* overwrite elements that are now gone) we now want all of the
* lines after this element up to the end tag.
*/
relativeTexts.linesBelowArray = getEndBlockBelow(relativeTexts.linesBelowArray);
/**
* Now we take the lines above our current element up until the
* start region tag and render that into an HTML element. We will
* use these elements to determine where to place our current element.
*/
this.appendToRegionalManager(el, regionalContainer, ctx, relativeTexts, sourcePath, startBockAbove, (domObj) => {
onUnloadElement(domObj, regionalContainer);
});
return;
}));
}
appendToRegionalManager(el, regionalContainer, ctx, relativeLines, sourcePath, parentStartBlock, onUnloadCallback) {
let { linesAboveArray, linesOfElement, linesBelowArray, textOfElement } = relativeLines;
let siblingsAbove = renderMarkdownFromLines(linesAboveArray, sourcePath);
let siblingsBelow = renderMarkdownFromLines(linesBelowArray, sourcePath);
let regionalManager = regionalContainer.getRegion();
/**
* Set up our dom object to be added to the manager.
*/
let currentObject = new DOMObject(el, linesOfElement);
el.id = currentObject.UID;
currentObject = TaskListDOMObject.checkForTaskListElement(currentObject);
/**
* Now we add the object to the manager and then setup the
* callback for when the object is removed from view that will remove
* the item from the manager.
*/
regionalManager.addObject(siblingsAbove, siblingsBelow, currentObject);
let elementMarkdownRenderer = new obsidian.MarkdownRenderChild(el);
elementMarkdownRenderer.onunload = () => {
onUnloadCallback(currentObject);
};
ctx.addChild(elementMarkdownRenderer);
/**
* Now we check if our current element is a special flag so we can
* properly set the element tag within the regional manager.
*/
if (containsEndTag(el.textContent) === true &&
parentStartBlock.startBlockType !== "PADOC") {
currentObject.elementType = "unRendered";
regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.endRegion);
}
if (isValidPandocEndTag(linesAboveArray, el.textContent) === true &&
parentStartBlock.startBlockType === "PADOC") {
currentObject.elementType = "unRendered";
regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.endRegion);
}
else if (containsColEndTag(textOfElement) === true) {
currentObject.elementType = "unRendered";
regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.columnBreak);
}
else if (containsColSettingsTag(textOfElement) === true) {
currentObject.elementType = "unRendered";
regionalManager = regionalContainer.setRegionSettings(textOfElement);
regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.regionSettings);
}
setElementCSS(currentObject, el);
parseColBreakErrorType({
lineAbove: linesAboveArray.last(),
lineBelow: linesBelowArray.first(),
objectTag: currentObject.tag,
colBreakType: currentObject.elementIsColumnBreak
}, regionalManager.errorManager);
regionalManager.renderRegionElementsToScreen();
return regionalManager;
}
setupMarkdownCodeblockPostProcessor(startStr) {
this.registerMarkdownCodeBlockProcessor(startStr, (source, el, ctx) => {
var _a;
if (this.settingsManager.renderOnMobile === false &&
obsidian.Platform.isMobile === true) {
return;
}
const sourcePath = ctx.sourcePath;
// Set up our CSS so that the codeblock only renders this data in reading mode
// source/live preview mode is handled by the CM6 implementation.
(_a = el.parentElement) === null || _a === void 0 ? void 0 : _a.addClass("preivew-mcm-start-block");
// To determine what kind of view we are rendering in we need a markdown leaf.
// Really this should never return here since rendering is only done in markdown leaves.
let markdownLeaves = app.workspace.getLeavesOfType("markdown");
if (markdownLeaves.length === 0) {
return;
}
if (this.globalManager === null || this.globalManager === undefined) {
// console.log("Global manager is undefined?");
return;
}
let fileDOMManager = this.globalManager.getFileManager(sourcePath);
if (fileDOMManager === null) {
return;
}
if (ctx.frontmatter &&
ctx.frontmatter["Multi-Column Reflow"] !== undefined) {
return;
}
else {
fileDOMManager.removeRegion("Multi-Column Reflow Region");
}
// Set file to have start tag.
fileDOMManager.setHasStartTag();
// Get the info for our current context and then check
// if the entire text contains a start tag. If there is
// no start tag in the document we can just return and
// ignore the rest of the parsing.
let info = ctx.getSectionInfo(el);
/**
* We need the context info to properly parse so returning here
* info is null. TODO: Set error in view if this occurs.
*/
if (!info) {
return;
}
/**
* Set up the current element to act as the parent for the
* multi-column region.
*/
el.classList.add(MultiColumnLayoutCSS.RegionRootContainerDiv);
let errorManager = new RegionErrorManager(el, ["The codeblock region start syntax has been depricated. Please manually update to the current syntax defined in the ReadMe, run the \"Fix Multi-Column Syntax in Current File\" from the Command Palette, or use the \"Update Depricated Syntax\" command found in the plugin settings window. You must reload the file for changes to take effect."]);
let renderColumnRegion = el.createDiv({
cls: MultiColumnLayoutCSS.RegionContentContainerDiv
});
let regionKey = parseStartRegionCodeBlockID(source);
let createNewRegionManager = true;
if (fileDOMManager.checkKeyExists(regionKey) === true) {
createNewRegionManager = false;
let { numberOfTags, keys } = countStartTags(info.text);
let numMatches = 0;
for (let i = 0; i < numberOfTags; i++) {
// Because we checked if key exists one of these has to match.
if (keys[i] === regionKey) {
numMatches++;
}
}
// We only want to display an error if there are more than 2 of the same id across
// the whole document. This prevents erros when obsidian reloads the whole document
// and there are two of the same key in the map.
if (numMatches >= 2) {
if (regionKey === "") {
errorManager.addErrorMessage("Found multiple regions with empty IDs. Please set a unique ID in the codeblock.\nEG: 'ID: randomID'");
}
else {
errorManager.addErrorMessage("Region ID already exists in document, please set a unique ID.");
}
return;
}
}
el.id = `MultiColumnID:${regionKey}`;
// If something changes in the codeblock we dont necessarily want to update our
// old reference to the region manager. This could be a potential bug area.
if (createNewRegionManager === true) {
// Create a new regional manager.
let elementMarkdownRenderer = new obsidian.MarkdownRenderChild(el);
fileDOMManager.createRegionalManager(regionKey, el, errorManager, renderColumnRegion);
// Set up the on unload callback. This can be called if the user changes
// the start/settings codeblock in any way. We only want to unload
// if the file is being removed from view.
elementMarkdownRenderer.onunload = () => {
if (fileDOMManager && fileStillInView(sourcePath) === false) {
// console.debug("File not in any markdown leaf. Removing region from dom manager.")
fileDOMManager.removeRegion(regionKey);
}
};
ctx.addChild(elementMarkdownRenderer);
}
let regionalManagerContainer = fileDOMManager.getRegionalContainer(regionKey);
if (regionalManagerContainer !== null) {
let regionalManager = regionalManagerContainer.setRegionSettings(source);
regionalManager.regionParent = renderColumnRegion;
}
});
}
loadSettings() {
return __awaiter(this, void 0, void 0, function* () {
this.settingsManager.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
});
}
saveSettings() {
return __awaiter(this, void 0, void 0, function* () {
yield this.saveData(this.settingsManager.settings);
});
}
renderDocReflow(el, ctx, sourcePath, fileDOMManager, docString, info) {
let regionalContainer = null;
if (fileDOMManager.checkKeyExists("Multi-Column Reflow Region") === true &&
el.getElementsByClassName("frontmatter").length === 0) {
regionalContainer = fileDOMManager.getRegionalContainer("Multi-Column Reflow Region");
}
else if (fileDOMManager.checkKeyExists("Multi-Column Reflow Region") === true &&
el.getElementsByClassName("frontmatter").length === 1) {
let parentEl = createDiv();
el.appendChild(parentEl);
// Get current data, remove old region.
regionalContainer = fileDOMManager.getRegionalContainer("Multi-Column Reflow Region");
let domList = regionalContainer.getRegion().getRegionData().domList.slice();
fileDOMManager.removeRegion("Multi-Column Reflow Region");
// Create new region.
setupStartTag(parentEl, ctx, fileDOMManager, docString, "Multi-Column Reflow Region");
regionalContainer = fileDOMManager.getRegionalContainer("Multi-Column Reflow Region");
let settings = getMultiColumnSettingsFromFrontmatter(ctx);
let leaf = getLeafFromFilePath(this.app.workspace, ctx.sourcePath);
let clientHeight = calcVisibleClietHeight(leaf, this.app.workspace);
if (settings.columnHeight === null) {
settings.columnHeight = HTMLSizing.create().setWidth(clientHeight).setUnits("px");
}
else {
settings.columnHeight = settings.columnHeight.convertToPX(this.app.workspace.containerEl);
}
regionalContainer.setRegionParsedSettings(settings);
// Re-Render after small delay.
// Delay is so the auto layout check can properly read the client height.
function delayRender() {
return __awaiter(this, void 0, void 0, function* () {
setTimeout(() => {
// Append all items to region.
let regionalManager = regionalContainer.getRegion();
let listLength = domList.length;
for (let i = 0; i < listLength; i++) {
let domObj = domList.shift();
regionalManager.addObjectAtIndex(domObj, i);
setElementCSS(domObj, domObj.originalElement);
}
regionalContainer.getRegion().renderRegionElementsToScreen();
}, 50);
});
}
delayRender();
return;
}
else {
// The first element to hit this point appears to be the yaml information which we can use
// as our root div since the whole doc is going to be re-formatted.
let parentEl = createDiv();
el.appendChild(parentEl);
setupStartTag(parentEl, ctx, fileDOMManager, docString, "Multi-Column Reflow Region");
regionalContainer = fileDOMManager.getRegionalContainer("Multi-Column Reflow Region");
let settings = getMultiColumnSettingsFromFrontmatter(ctx);
let leaf = getLeafFromFilePath(this.app.workspace, ctx.sourcePath);
let clientHeight = calcVisibleClietHeight(leaf, this.app.workspace);
if (settings.columnHeight === null) {
settings.columnHeight = HTMLSizing.create().setWidth(clientHeight).setUnits("px");
}
else {
settings.columnHeight = settings.columnHeight.convertToPX(this.app.workspace.containerEl);
}
regionalContainer.setRegionParsedSettings(settings);
return;
}
if (regionalContainer === null) {
return;
}
let docLines = docString.split("\n");
let relativeTexts = extractElementRelativeLocationData(docLines, info);
relativeTexts.linesBelowArray = getEndBlockBelow(relativeTexts.linesBelowArray);
if (containsStartTag(relativeTexts.textOfElement) ||
containsColSettingsTag(relativeTexts.textOfElement)) {
if (containsStartTag(relativeTexts.textOfElement)) {
setElementCSSByTag(DOMObjectTag.startRegion, el);
}
else if (containsColSettingsTag(relativeTexts.textOfElement)) {
setElementCSSByTag(DOMObjectTag.regionSettings, el);
}
return;
}
let startBockAbove = {
linesAboveArray: relativeTexts.linesAboveArray,
startBlockKey: "Multi-Column Reflow Region",
startBlockType: "ORIGINAL"
};
this.appendToRegionalManager(el, regionalContainer, ctx, relativeTexts, sourcePath, startBockAbove, (domObj) => {
onUnloadElement(domObj, regionalContainer);
});
}
//#region PDF Exporting.
isStartCodeblockInExport(node) {
for (let i = 0; i < CODEBLOCK_START_STRS.length; i++) {
if (node.hasClass(`block-language-${CODEBLOCK_START_STRS[i]}`)) {
return true;
}
}
return false;
}
exportDocumentToPDF(el, fileDOMManager, sourcePath) {
return __awaiter(this, void 0, void 0, function* () {
// A true export will be passed an element with all other items in the doc as children.
// So if there are no children we can just return
let docChildren = Array.from(el.childNodes);
if (docChildren.length === 0) {
return;
}
let childrenToRemove = [];
// To export codeblocks we need to get the IDs so we can get the data from our managers.
// however since the ID isnt being stored in the element yet this means we need to read
// all of the IDs out of the full document.
let codeblockStartBlocks = [];
let aFile = this.app.vault.getAbstractFileByPath(sourcePath);
if (aFile instanceof obsidian.TFile) {
let file = aFile;
let fileText = yield this.app.vault.cachedRead(file); // Is cached read Ok here? It should be.
// Once we have our data we search the text for all codeblock start values.
// storing them into our queue.
let codeBlockData = findStartCodeblock(fileText);
while (codeBlockData.found === true) {
let codeblockText = fileText.slice(codeBlockData.startPosition, codeBlockData.endPosition);
fileText = fileText.slice(codeBlockData.endPosition);
codeblockStartBlocks.push(codeblockText);
codeBlockData = findStartCodeblock(fileText);
}
}
else {
console.error(`Error getting file from source path: ${sourcePath}`);
}
let inBlock = false;
for (let i = 0; i < docChildren.length; i++) {
let child = docChildren[i];
if (child instanceof HTMLElement) {
if (inBlock === false) {
let foundBlockData = false;
let regionKey = "";
let blockData = isStartTagWithID(child.textContent);
let pandocData = getPandocStartData(child.textContent);
if (blockData.isStartTag === true) {
// If an old-style start tag.
foundBlockData = true;
if (blockData.hasKey === true) {
let foundKey = getStartTagKey(child.textContent);
if (foundKey !== null) {
regionKey = foundKey;
}
}
}
else if (blockData.isStartTag === false && this.isStartCodeblockInExport(child)) {
// If the start tag from the old version is null we then check to see if the element is
// a codeblock start. If it is we use the next available codeblock data to retrieve our ID.
let codeblockText = codeblockStartBlocks.shift();
if (codeblockText === undefined) {
console.error("Found undefined codeblock data when exporting.");
return;
}
let id = parseStartRegionCodeBlockID(codeblockText);
if (id !== "") {
foundBlockData = true;
regionKey = id;
}
}
else if (pandocData.found) {
foundBlockData = true;
regionKey = pandocData.userSettings.columnID;
}
if (foundBlockData === true && regionKey !== "") {
inBlock = true;
for (let i = child.children.length - 1; i >= 0; i--) {
child.children[i].detach();
}
child.innerText = "";
child.classList.add(MultiColumnLayoutCSS.RegionRootContainerDiv);
let errorManager = new RegionErrorManager(el);
let renderColumnRegion = child.createDiv({
cls: MultiColumnLayoutCSS.RegionContentContainerDiv
});
let regionalContainer = fileDOMManager.getRegionalContainer(regionKey);
if (regionalContainer === null || regionalContainer.getRegion().numberOfChildren === 0) {
// If the number of children is 0, we are probably in LivePreview, where the codeblock start regions have been processed by native obsidian live preview but do not have any children linked to them.
errorManager.addErrorMessage("Error rendering multi-column region.\nPlease close and reopen the file, then make sure you are in reading mode before exporting.");
}
else {
let regionalManager = regionalContainer.getRegion();
regionalManager.exportRegionElementsToPDF(renderColumnRegion);
}
}
}
else {
if (containsEndTag(child.textContent) === true ||
containsPandocEndTag(child.textContent) === true) {
inBlock = false;
}
childrenToRemove.push(child);
}
}
}
childrenToRemove.forEach(child => {
if (child.parentElement === el) {
el.removeChild(child);
}
});
});
}
checkExporting(element) {
if (element === null) {
return false;
}
if (element.classList.contains("print")) {
return true;
}
if (element.parentNode !== null) {
return this.checkExporting(element.parentElement);
}
return false;
}
}
function setElementCSS(currentObject, el) {
setElementCSSByTag(currentObject.tag, el);
}
function setElementCSSByTag(tag, el) {
if (tag === DOMObjectTag.endRegion) {
el.addClass(MultiColumnStyleCSS.RegionEndTag);
}
else if (tag === DOMObjectTag.columnBreak) {
el.addClass(MultiColumnStyleCSS.ColumnEndTag);
}
else if (tag === DOMObjectTag.regionSettings) {
el.addClass(MultiColumnStyleCSS.RegionSettings);
}
else {
el.addClass(MultiColumnStyleCSS.RegionContent);
}
}
function onUnloadElement(currentObject, regionalContainer) {
if (regionalContainer === null) {
return;
}
let regionalManager = regionalContainer.getRegion();
if (regionalManager) {
// We can attempt to update the view here after the item is removed
// but need to get the item's parent element before removing object from manager.
let regionRenderData = regionalManager.getRegionRenderData();
regionalManager.removeObject(currentObject.UID);
/**
* Need to check here if element is null as this closure will be called
* repeatedly on file change.
*/
if (regionRenderData.parentRenderElement === null) {
return;
}
regionalManager.renderRegionElementsToScreen();
}
}
function extractElementRelativeLocationData(docLines, info) {
let linesAboveArray = docLines.slice(0, info.lineStart);
let linesOfElement = docLines.slice(info.lineStart, info.lineEnd + 1);
let textOfElement = linesOfElement.join("\n");
let linesBelowArray = docLines.slice(info.lineEnd + 1);
return {
linesAboveArray,
linesOfElement,
linesBelowArray,
textOfElement
};
}
function createStartElement(el, linesOfElement, ctx, fileDOMManager, docString) {
el.children[0].detach();
let startBlockData = getStartBlockAboveLine(linesOfElement);
if (startBlockData === null) {
return;
}
let regionID = startBlockData.startBlockKey;
setupStartTag(el, ctx, fileDOMManager, docString, regionID);
return;
}
function createPandocStartElement(el, textOfElement, ctx, fileDOMManager, docString) {
el.children[0].detach();
let pandocData = getPandocStartData(textOfElement);
let settings = pandocData.userSettings;
let regionManager = setupStartTag(el, ctx, fileDOMManager, docString, settings.columnID);
regionManager.setRegionalSettings(settings);
return;
}
function renderMarkdownFromLines(mdLines, sourcePath) {
/**
* We re-render all of the items above our element, until the start tag,
* so we can determine where to place the new item in the manager.
*
* TODO: Can reduce the amount needing to be rendered by only rendering to
* the start tag or a column-break whichever is closer.
*/
let siblings = createDiv();
let markdownRenderChild = new obsidian.MarkdownRenderChild(siblings);
obsidian.MarkdownRenderer.renderMarkdown(mdLines.reduce((prev, current) => {
return prev + "\n" + current;
}, ""), siblings, sourcePath, markdownRenderChild);
return siblings;
}
function setupStartTag(el, ctx, fileDOMManager, docString, regionID) {
/**
* Set up the current element to act as the parent for the
* multi-column region.
*/
el.classList.add(MultiColumnLayoutCSS.RegionRootContainerDiv);
let errorManager = new RegionErrorManager(el);
let renderColumnRegion = el.createDiv({
cls: MultiColumnLayoutCSS.RegionContentContainerDiv
});
if (fileDOMManager.checkKeyExists(regionID) === true) {
let { numberOfTags, keys } = countStartTags(docString);
let numMatches = 0;
for (let i = 0; i < numberOfTags; i++) {
// Because we checked if key exists one of these has to match.
if (keys[i] === regionID) {
numMatches++;
}
}
// We only want to display an error if there are more than 2 of the same id across
// the whole document. This prevents erros when obsidian reloads the whole document
// and there are two of the same key in the map.
if (numMatches >= 2) {
if (regionID === "") {
errorManager.addErrorMessage("Found multiple regions with empty IDs. Please set a unique ID after each start tag.\nEG: '--- multi-column-start: randomID'\nOr use 'Fix Missing IDs' in the command palette and reload the document.");
}
else {
errorManager.addErrorMessage("Region ID already exists in document, please set a unique ID.\nEG: '--- multi-column-start: randomID'");
}
return;
}
}
el.id = `MultiColumnID:${regionID}`;
let elementMarkdownRenderer = new obsidian.MarkdownRenderChild(el);
let regionManager = fileDOMManager.createRegionalManager(regionID, el, errorManager, renderColumnRegion);
elementMarkdownRenderer.onunload = () => {
if (fileDOMManager) {
fileDOMManager.removeRegion(regionID);
}
};
ctx.addChild(elementMarkdownRenderer);
return regionManager;
}
const FRONTMATTER_REGEX = [
/Multi[- ]*Column *Markdown/i,
/Multi[- ]*Column *Reflow/i
];
function isMultiColumnReflow(frontmatter) {
if (frontmatter === null ||
frontmatter === undefined) {
return false;
}
for (let regex of FRONTMATTER_REGEX) {
let frontmatterReflowData = obsidian.parseFrontMatterEntry(frontmatter, regex);
if (frontmatterReflowData !== null) {
return true;
}
}
let [keys, values] = Object.entries(frontmatter);
if (keys === undefined) {
return false;
}
for (let key of keys) {
if (typeof key !== "string") {
continue;
}
for (let regex of FRONTMATTER_REGEX) {
let regexResult = regex.exec(key);
if (regexResult !== null) {
return true;
}
}
}
return false;
}
function getMultiColumnSettingsFromFrontmatter(ctx) {
let settings = getDefaultMultiColumnSettings();
settings.fullDocReflow = true;
if (ctx.frontmatter === null ||
ctx.frontmatter === undefined) {
return settings;
}
for (let regex of FRONTMATTER_REGEX) {
let frontmatterReflowData = obsidian.parseFrontMatterEntry(ctx.frontmatter, regex);
if (frontmatterReflowData !== null &&
Array.isArray(frontmatterReflowData)) {
settings = parseFrontmatterSettings(frontmatterReflowData);
settings.fullDocReflow = true;
break;
}
}
return settings;
}
function parseFrontmatterSettings(frontmatterReflowData) {
let str = "";
for (let obj of frontmatterReflowData) {
let [key, value] = Object.entries(obj)[0];
str += `${key}: [${value}]\n`;
}
let settings = parseColumnSettings(str);
return settings;
}
function getContentHeightFromLeaf(leaf) {
let contentEl = leaf.view["contentEl"];
if (contentEl !== undefined &&
contentEl.clientHeight > 0) {
return contentEl.clientHeight;
}
let clientHeight = leaf.view.containerEl.clientHeight;
let titleContainer = leaf.view["titleContainerEl"];
if (titleContainer !== undefined &&
titleContainer.clientHeight > 0) {
return clientHeight - titleContainer.clientHeight;
}
return clientHeight - 50;
}
function calcVisibleClietHeight(leaf, workspace) {
let clientHeight = 0;
if (leaf) {
clientHeight = getContentHeightFromLeaf(leaf);
}
else if ((workspace !== null && workspace !== undefined) &&
(workspace.containerEl !== null && workspace.containerEl !== undefined) &&
workspace.containerEl.clientHeight > 0) {
clientHeight = workspace.containerEl.clientHeight - 100;
}
else {
clientHeight = 1000;
}
return clientHeight;
}
module.exports = MultiColumnMarkdown;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsibm9kZV9tb2R1bGVzL3RzbGliL3RzbGliLmVzNi5qcyIsIm5vZGVfbW9kdWxlcy9AcGFjb3RlL2dldC1zdHlsZS9saWIvZXNtL2luZGV4LmpzIiwibm9kZV9tb2R1bGVzL0BwYWNvdGUvcGl4ZWxzL2xpYi9lc20vaW5kZXguanMiLCJzcmMvdXRpbGl0aWVzL2ludGVyZmFjZXMudHMiLCJzcmMvcmVnaW9uU2V0dGluZ3MudHMiLCJzcmMvdXRpbGl0aWVzL3NldHRpbmdzUGFyc2VyLnRzIiwic3JjL3V0aWxpdGllcy9wYW5kb2NQYXJzZXIudHMiLCJzcmMvdXRpbGl0aWVzL3RleHRQYXJzZXIudHMiLCJzcmMvdXRpbGl0aWVzL3V0aWxzLnRzIiwic3JjL2RvbV9tYW5hZ2VyL2RvbU9iamVjdC50cyIsInNyYy91dGlsaXRpZXMvY3NzRGVmaW5pdGlvbnMudHMiLCJzcmMvdXRpbGl0aWVzL2VsZW1lbnRSZW5kZXJUeXBlUGFyc2VyLnRzIiwic3JjL2RvbV9tYW5hZ2VyL3JlZ2lvbmFsX21hbmFnZXJzL3JlZ2lvbk1hbmFnZXIudHMiLCJzcmMvcGx1Z2luU2V0dGluZ3MudHMiLCJzcmMvZG9tX21hbmFnZXIvcmVnaW9uYWxfbWFuYWdlcnMvc3RhbmRhcmRNdWx0aUNvbHVtblJlZ2lvbk1hbmFnZXIudHMiLCJzcmMvZG9tX21hbmFnZXIvcmVnaW9uYWxfbWFuYWdlcnMvc2luZ2xlQ29sdW1uUmVnaW9uTWFuYWdlci50cyIsInNyYy9kb21fbWFuYWdlci9yZWdpb25hbF9tYW5hZ2Vycy9hdXRvTGF5b3V0UmVnaW9uTWFuYWdlci50cyIsInNyYy9kb21fbWFuYWdlci9yZWdpb25hbF9tYW5hZ2Vycy9yZWZsb3dSZWdpb25NYW5hZ2VyLnRzIiwic3JjL2RvbV9tYW5hZ2VyL3JlZ2lvbmFsX21hbmFnZXJzL3JlZ2lvbk1hbmFnZXJDb250YWluZXIudHMiLCJzcmMvZG9tX21hbmFnZXIvZG9tTWFuYWdlci50cyIsInNyYy9kb21fbWFuYWdlci9yZWdpb25FcnJvck1hbmFnZXIudHMiLCJzcmMvdXRpbGl0aWVzL2Vycm9yTWVzc2FnZS50cyIsInNyYy91dGlsaXRpZXMvb2JzaVV0aWxzLnRzIiwic3JjL2xpdmVfcHJldmlldy9tY21fbGl2ZVByZXZpZXdfd2lkZ2V0LnRzIiwic3JjL2xpdmVfcHJldmlldy9jbTZfbGl2ZVByZXZpZXcudHMiLCJzcmMvbGl2ZV9wcmV2aWV3L2NtNl9saXZlUHJlaXZld19vbkNsaWNrRml4LnRzIiwic3JjL3V0aWxpdGllcy9zeW50YXhVcGRhdGUudHMiLCJzcmMvc2V0dGluZ3MvTXVsdGlDb2x1bW5TZXR0aW5nc1ZpZXcudHMiLCJzcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6WyIvKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqXHJcbkNvcHlyaWdodCAoYykgTWljcm9zb2Z0IENvcnBvcmF0aW9uLlxyXG5cclxuUGVybWlzc2lvbiB0byB1c2UsIGNvcHksIG1vZGlmeSwgYW5kL29yIGRpc3RyaWJ1dGUgdGhpcyBzb2Z0d2FyZSBmb3IgYW55XHJcbnB1cnBvc2Ugd2l0aCBvciB3aXRob3V0IGZlZSBpcyBoZXJlYnkgZ3JhbnRlZC5cclxuXHJcblRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCBcIkFTIElTXCIgQU5EIFRIRSBBVVRIT1IgRElTQ0xBSU1TIEFMTCBXQVJSQU5USUVTIFdJVEhcclxuUkVHQVJEIFRPIFRISVMgU09GVFdBUkUgSU5DTFVESU5HIEFMTCBJTVBMSUVEIFdBUlJBTlRJRVMgT0YgTUVSQ0hBTlRBQklMSVRZXHJcbkFORCBGSVRORVNTLiBJTiBOTyBFVkVOVCBTSEFMTCBUSEUgQVVUSE9SIEJFIExJQUJMRSBGT1IgQU5ZIFNQRUNJQUwsIERJUkVDVCxcclxuSU5ESVJFQ1QsIE9SIENPTlNFUVVFTlRJQUwgREFNQUdFUyBPUiBBTlkgREFNQUdFUyBXSEFUU09FVkVSIFJFU1VMVElORyBGUk9NXHJcbkxPU1MgT0YgVVNFLCBEQVRBIE9SIFBST0ZJVFMsIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBORUdMSUdFTkNFIE9SXHJcbk9USEVSIFRPUlRJT1VTIEFDVElPTiwgQVJJU0lORyBPVVQgT0YgT1IgSU4gQ09OTkVDVElPTiBXSVRIIFRIRSBVU0UgT1JcclxuUEVSRk9STUFOQ0UgT0YgVEhJUyBTT0ZUV0FSRS5cclxuKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKiogKi9cclxuLyogZ2xvYmFsIFJlZmxlY3QsIFByb21pc2UgKi9cclxuXHJcbnZhciBleHRlbmRTdGF0aWNzID0gZnVuY3Rpb24oZCwgYikge1xyXG4gICAgZXh0ZW5kU3RhdGljcyA9IE9iamVjdC5zZXRQcm90b3R5cGVPZiB8fFxyXG4gICAgICAgICh7IF9fcHJvdG9fXzogW10gfSBpbnN0YW5jZW9mIEFycmF5ICYmIGZ1bmN0aW9uIChkLCBiKSB7IGQuX19wcm90b19fID0gYjsgfSkgfHxcclxuICAgICAgICBmdW5jdGlvbiAoZCwgYikgeyBmb3IgKHZhciBwIGluIGIpIGlmIChPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoYiwgcCkpIGRbcF0gPSBiW3BdOyB9O1xyXG4gICAgcmV0dXJuIGV4dGVuZFN0YXRpY3MoZCwgYik7XHJcbn07XHJcblxyXG5leHBvcnQgZnVuY3Rpb24gX19leHRlbmRzKGQsIGIpIHtcclxuICAgIGlmICh0eXBlb2YgYiAhPT0gXCJmdW5jdGlvblwiICYmIGIgIT09IG51bGwpXHJcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcIkNsYXNzIGV4dGVuZHMgdmFsdWUgXCIgKyBTdHJpbmcoYikgKyBcIiBpcyBub3QgYSBjb25zdHJ1Y3RvciBvciBudWxsXCIpO1xyXG4gICAgZXh0ZW5kU3RhdGljcyhkLCBiKTtcclxuICAgIGZ1bmN0aW9uIF9fKCkgeyB0aGlzLmNvbnN0cnVjdG9yID0gZDsgfVxyXG4gICAgZC5wcm90b3R5cGUgPSBiID09PSBudWxsID8gT2JqZWN0LmNyZWF0ZShiKSA6IChfXy5wcm90b3R5cGUgPSBiLnByb3RvdHlwZSwgbmV3IF9fKCkpO1xyXG59XHJcblxyXG5leHBvcnQgdmFyIF9fYXNzaWduID0gZnVuY3Rpb24oKSB7XHJcbiAgICBfX2Fzc2lnbiA9IE9iamVjdC5hc3NpZ24gfHwgZnVuY3Rpb24gX19hc3NpZ24odCkge1xyXG4gICAgICAgIGZvciAodmFyIHMsIGkgPSAxLCBuID0gYXJndW1lbnRzLmxlbmd0aDsgaSA8IG47IGkrKykge1xyXG4gICAgICAgICAgICBzID0gYXJndW1lbnRzW2ldO1xyXG4gICAgICAgICA