You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3186 lines
496 KiB
3186 lines
496 KiB
2 years ago
|
/*
|
||
|
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());
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* 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 ColumnLayout;
|
||
|
(function (ColumnLayout) {
|
||
|
ColumnLayout[ColumnLayout["standard"] = 0] = "standard";
|
||
|
ColumnLayout[ColumnLayout["left"] = 1] = "left";
|
||
|
ColumnLayout[ColumnLayout["first"] = 2] = "first";
|
||
|
ColumnLayout[ColumnLayout["center"] = 3] = "center";
|
||
|
ColumnLayout[ColumnLayout["middle"] = 4] = "middle";
|
||
|
ColumnLayout[ColumnLayout["second"] = 5] = "second";
|
||
|
ColumnLayout[ColumnLayout["right"] = 6] = "right";
|
||
|
ColumnLayout[ColumnLayout["third"] = 7] = "third";
|
||
|
ColumnLayout[ColumnLayout["last"] = 8] = "last";
|
||
|
})(ColumnLayout || (ColumnLayout = {}));
|
||
|
var SingleColumnSize;
|
||
|
(function (SingleColumnSize) {
|
||
|
SingleColumnSize[SingleColumnSize["small"] = 0] = "small";
|
||
|
SingleColumnSize[SingleColumnSize["medium"] = 1] = "medium";
|
||
|
SingleColumnSize[SingleColumnSize["large"] = 2] = "large";
|
||
|
})(SingleColumnSize || (SingleColumnSize = {}));
|
||
|
var ContentOverflowType;
|
||
|
(function (ContentOverflowType) {
|
||
|
ContentOverflowType[ContentOverflowType["scroll"] = 0] = "scroll";
|
||
|
ContentOverflowType[ContentOverflowType["hidden"] = 1] = "hidden";
|
||
|
})(ContentOverflowType || (ContentOverflowType = {}));
|
||
|
function getDefaultMultiColumnSettings() {
|
||
|
return {
|
||
|
numberOfColumns: 2,
|
||
|
columnLayout: ColumnLayout.standard,
|
||
|
drawBorder: true,
|
||
|
drawShadow: true,
|
||
|
autoLayout: false,
|
||
|
columnSize: SingleColumnSize.medium,
|
||
|
columnPosition: ColumnLayout.standard,
|
||
|
columnSpacing: "",
|
||
|
contentOverflow: ContentOverflowType.scroll
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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"
|
||
|
];
|
||
|
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"
|
||
|
];
|
||
|
const NUMBER_OF_COLUMNS_REGEX_ARR = NUMBER_OF_COLUMNS_STRS.map(convertStringToSettingsRegex).map((value) => {
|
||
|
return new RegExp(value, "i");
|
||
|
});
|
||
|
const LARGEST_COLUMN_STRS = [
|
||
|
"largest column"
|
||
|
];
|
||
|
const LARGEST_COLUMN_REGEX_ARR = LARGEST_COLUMN_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"
|
||
|
];
|
||
|
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",
|
||
|
].map((value) => {
|
||
|
return new RegExp(convertStringToSettingsRegex(value), "i");
|
||
|
});
|
||
|
const CONTENT_OVERFLOW_REGEX_ARR = [
|
||
|
"overflow",
|
||
|
"content overflow"
|
||
|
].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) {
|
||
|
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];
|
||
|
let settingsData = getSettingsDataFromKeys(settingsLine, NUMBER_OF_COLUMNS_REGEX_ARR);
|
||
|
if (settingsData !== null) {
|
||
|
let numOfCols = parseInt(settingsData);
|
||
|
if (Number.isNaN(numOfCols) === false) {
|
||
|
if (numOfCols >= 1 && numOfCols <= 3) {
|
||
|
parsedSettings.numberOfColumns = numOfCols;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
settingsData = getSettingsDataFromKeys(settingsLine, LARGEST_COLUMN_REGEX_ARR);
|
||
|
if (settingsData !== null) {
|
||
|
let userDefLayout = ColumnLayout[settingsData];
|
||
|
if (userDefLayout !== undefined) {
|
||
|
parsedSettings.columnLayout = userDefLayout;
|
||
|
parsedSettings.columnPosition = userDefLayout;
|
||
|
}
|
||
|
}
|
||
|
settingsData = getSettingsDataFromKeys(settingsLine, DRAW_BORDER_REGEX_ARR);
|
||
|
if (settingsData !== null) {
|
||
|
let isBorderDrawn = BorderOption[settingsData];
|
||
|
if (isBorderDrawn !== undefined) {
|
||
|
switch (isBorderDrawn) {
|
||
|
case (BorderOption.disabled):
|
||
|
case (BorderOption.off):
|
||
|
case (BorderOption.false):
|
||
|
parsedSettings.drawBorder = false;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
settingsData = getSettingsDataFromKeys(settingsLine, DRAW_SHADOW_REGEX_ARR);
|
||
|
if (settingsData !== null) {
|
||
|
let isShadowDrawn = ShadowOption[settingsData];
|
||
|
if (isShadowDrawn !== undefined) {
|
||
|
switch (isShadowDrawn) {
|
||
|
case (ShadowOption.disabled):
|
||
|
case (ShadowOption.off):
|
||
|
case (ShadowOption.false):
|
||
|
parsedSettings.drawShadow = false;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
settingsData = getSettingsDataFromKeys(settingsLine, AUTO_LAYOUT_REGEX_ARR);
|
||
|
if (settingsData !== null) {
|
||
|
if (settingsData === "true") {
|
||
|
parsedSettings.autoLayout = true;
|
||
|
}
|
||
|
}
|
||
|
settingsData = getSettingsDataFromKeys(settingsLine, COLUMN_SPACING_REGEX_ARR);
|
||
|
if (settingsData !== null) {
|
||
|
let parsed = getLengthUnit(settingsData.trim());
|
||
|
let spacingStr = "";
|
||
|
if (parsed.isValid) {
|
||
|
let noUnitsStr = settingsData.replace(parsed.unitStr, "").trim();
|
||
|
let noUnitsNum = parseInt(noUnitsStr);
|
||
|
if (isNaN(noUnitsNum) === false) {
|
||
|
spacingStr = `${noUnitsStr}${parsed.unitStr}`;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
let noUnitsNum = parseInt(settingsData.trim());
|
||
|
if (isNaN(noUnitsNum) === false) {
|
||
|
spacingStr = `${noUnitsNum}pt`;
|
||
|
}
|
||
|
}
|
||
|
parsedSettings.columnSpacing = spacingStr;
|
||
|
}
|
||
|
settingsData = getSettingsDataFromKeys(settingsLine, CONTENT_OVERFLOW_REGEX_ARR);
|
||
|
if (settingsData !== null) {
|
||
|
let overflowType = ContentOverflowType.scroll;
|
||
|
settingsData = settingsData.toLowerCase().trim();
|
||
|
if (settingsData === "hidden") {
|
||
|
overflowType = ContentOverflowType.hidden;
|
||
|
}
|
||
|
parsedSettings.contentOverflow = overflowType;
|
||
|
}
|
||
|
}
|
||
|
return parsedSettings;
|
||
|
}
|
||
|
function 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 };
|
||
|
}
|
||
|
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 ColumnLayout.left;
|
||
|
case "middle":
|
||
|
case "middlealigned":
|
||
|
case "middlealignment":
|
||
|
case "center":
|
||
|
case "centeraligned":
|
||
|
case "centeralignment":
|
||
|
case "centered":
|
||
|
case "standard":
|
||
|
return ColumnLayout.center;
|
||
|
case "right":
|
||
|
case "rightside":
|
||
|
case "rightmargin":
|
||
|
case "rightalign":
|
||
|
case "rightaligned":
|
||
|
case "rightalignment":
|
||
|
case "last":
|
||
|
case "end":
|
||
|
return ColumnLayout.right;
|
||
|
}
|
||
|
return ColumnLayout.center;
|
||
|
}
|
||
|
function parseForSingleColumnSize(sizeString) {
|
||
|
switch (sizeString = sizeString.toLowerCase().trim().replace(" ", "")) {
|
||
|
case "small":
|
||
|
case "sm":
|
||
|
return SingleColumnSize.small;
|
||
|
case "medium":
|
||
|
case "med":
|
||
|
return SingleColumnSize.medium;
|
||
|
case "large":
|
||
|
case "lg":
|
||
|
return SingleColumnSize.large;
|
||
|
}
|
||
|
return SingleColumnSize.medium;
|
||
|
}
|
||
|
function convertStringToSettingsRegex(originalString) {
|
||
|
originalString = originalString.replace(" ", " *");
|
||
|
let regexString = `(?:${originalString} *: *)(.*)`;
|
||
|
return regexString;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* 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 found = false;
|
||
|
let startPosition = -1;
|
||
|
let matchLength = 0;
|
||
|
for (let i = 0; i < START_REGEX_ARR.length; i++) {
|
||
|
let regexData = START_REGEX_ARR[i].exec(text);
|
||
|
if (regexData !== null && regexData.length > 0) {
|
||
|
startPosition = regexData.index;
|
||
|
matchLength = regexData[0].length;
|
||
|
let line = text.slice(startPosition, startPosition + matchLength);
|
||
|
if (START_REGEX_ARR_WHOLE_LINE[i].test(line)) {
|
||
|
found = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
let endPosition = startPosition + matchLength;
|
||
|
return { found, startPosition, endPosition, matchLength };
|
||
|
}
|
||
|
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"];
|
||
|
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) {
|
||
|
let found = false;
|
||
|
let startPosition = -1;
|
||
|
for (let i = 0; i < END_REGEX_ARR.length; i++) {
|
||
|
if (END_REGEX_ARR[i].test(text)) {
|
||
|
found = true;
|
||
|
startPosition = text.search(END_REGEX_STRS[i]);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
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 };
|
||
|
}
|
||
|
function containsEndTag(text) {
|
||
|
return findEndTag(text).found;
|
||
|
}
|
||
|
const COL_REGEX_STRS = ["=== *column-end *===",
|
||
|
"=== *end-column *===",
|
||
|
"=== *column-break *===",
|
||
|
"=== *break-column *===",
|
||
|
"--- *column-end *---",
|
||
|
"--- *end-column *---",
|
||
|
"--- *column-break *---",
|
||
|
"--- *break-column *---"];
|
||
|
const COL_REGEX_ARR = [];
|
||
|
for (let i = 0; i < COL_REGEX_STRS.length; i++) {
|
||
|
COL_REGEX_ARR.push(new RegExp(COL_REGEX_STRS[i]));
|
||
|
}
|
||
|
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 COL_ELEMENT_INNER_TEXT_REGEX_STRS = ["= *column-end *=",
|
||
|
"= *end-column *=",
|
||
|
"= *column-break *=",
|
||
|
"= *break-column *="];
|
||
|
for (let i = 0; i < COL_ELEMENT_INNER_TEXT_REGEX_STRS.length; i++) {
|
||
|
COL_REGEX_ARR.push(new RegExp(COL_ELEMENT_INNER_TEXT_REGEX_STRS[i]));
|
||
|
}
|
||
|
function elInnerTextContainsColEndTag(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 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 };
|
||
|
}
|
||
|
const START_CODEBLOCK_REGEX_ARR = [
|
||
|
"```multi-column-start",
|
||
|
"```start-multi-column"
|
||
|
].map((val) => {
|
||
|
return new RegExp(val);
|
||
|
});
|
||
|
function findStartCodeblock(text) {
|
||
|
let found = false;
|
||
|
let startPosition = -1;
|
||
|
let endPosition = -1;
|
||
|
let matchLength = 0;
|
||
|
for (let i = 0; i < START_CODEBLOCK_REGEX_ARR.length; i++) {
|
||
|
let regexData = START_CODEBLOCK_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 };
|
||
|
}
|
||
|
function containsStartCodeBlock(text) {
|
||
|
return findStartCodeblock(text).found;
|
||
|
}
|
||
|
function containsRegionStart(text) {
|
||
|
return containsStartCodeBlock(text) || containsStartTag(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 getStartBlockOrCodeblockAboveLine(linesAboveArray) {
|
||
|
let startBlock = getStartBlockAboveLine(linesAboveArray);
|
||
|
if (startBlock !== null) {
|
||
|
return startBlock;
|
||
|
}
|
||
|
let codeBlock = getStartCodeBlockAboveLine(linesAboveArray);
|
||
|
if (codeBlock !== null) {
|
||
|
return codeBlock;
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
/**
|
||
|
* 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) {
|
||
|
// 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 linesAboveStr = linesAboveArray.reduce((prev, current) => {
|
||
|
return prev + "\n" + current;
|
||
|
}, "");
|
||
|
/*
|
||
|
* First thing we need to do is check if there are any end tags in the
|
||
|
* set of strings (which logically would close start tags and therefore
|
||
|
* the start tag it closes is not what we want). If there are we want to
|
||
|
* slowly narrow down our set of strings until the last end tag is
|
||
|
* removed. This makes it easier to find the closest open start tag
|
||
|
* in the data.
|
||
|
*/
|
||
|
let endTagSerachData = findEndTag(linesAboveStr);
|
||
|
while (endTagSerachData.found === true) {
|
||
|
// Get the index of where the first regex match in the
|
||
|
// string is. then we slice from 0 to index off of the string
|
||
|
// split it by newline, cut off the first line (which actually
|
||
|
// contains the regex) then reduce back down to a single string.
|
||
|
//
|
||
|
// TODO: This could be simplified if we just slice the text after
|
||
|
// the end tag instead of the begining.
|
||
|
let indexOfRegex = endTagSerachData.startPosition;
|
||
|
linesAboveArray = linesAboveStr.slice(indexOfRegex).split("\n").splice(1);
|
||
|
linesAboveStr = linesAboveArray.reduce((prev, current) => {
|
||
|
return prev + "\n" + current;
|
||
|
}, "");
|
||
|
endTagSerachData = findEndTag(linesAboveStr);
|
||
|
}
|
||
|
/**
|
||
|
* Now we have the set of lines after all other end tags. We now
|
||
|
* need to check if there is still a start tag left in the data. If
|
||
|
* there is no start tag then we want to return an empty array and empty
|
||
|
* key.
|
||
|
*/
|
||
|
let startBlockKey = "";
|
||
|
let startTagSearchData = findStartTag(linesAboveStr);
|
||
|
if (startTagSearchData.found === false) {
|
||
|
return null;
|
||
|
}
|
||
|
else {
|
||
|
/**
|
||
|
* Now we know there is at least 1 start key left, however there
|
||
|
* may be multiple start keys if the user is not closing their
|
||
|
* blocks. We currently dont allow recusive splitting so we
|
||
|
* want to get the last key in our remaining set. Same idea as
|
||
|
* above.
|
||
|
*/
|
||
|
while (startTagSearchData.found === true) {
|
||
|
// Get the index of where the first regex match in the
|
||
|
// string is. then we slice from 0 to index off of the string
|
||
|
// split it by newline, cut off the first line (which actually
|
||
|
// contains the regex) then reduce back down to a single string.
|
||
|
//
|
||
|
// TODO: This could be simplified if we just slice the text after
|
||
|
// the end tag instead of the begining.
|
||
|
let startIndex = startTagSearchData.startPosition;
|
||
|
linesAboveArray = linesAboveStr.slice(startIndex).split("\n");
|
||
|
let startTag = linesAboveArray[0];
|
||
|
let key = getStartTagKey(startTag);
|
||
|
if (key !== null) {
|
||
|
startBlockKey = key;
|
||
|
}
|
||
|
linesAboveArray = linesAboveArray.splice(1);
|
||
|
linesAboveStr = linesAboveArray.reduce((prev, current) => {
|
||
|
return prev + "\n" + current;
|
||
|
}, "");
|
||
|
startTagSearchData = findStartTag(linesAboveStr);
|
||
|
}
|
||
|
}
|
||
|
if (startBlockKey === "") {
|
||
|
let codeBlockData = parseCodeBlockStart(linesAboveArray);
|
||
|
if (codeBlockData !== null) {
|
||
|
startBlockKey = codeBlockData.id;
|
||
|
if (codeBlockData.index > 0) {
|
||
|
linesAboveArray = linesAboveArray.slice(codeBlockData.index + 1);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return { startBlockKey, linesAboveArray };
|
||
|
}
|
||
|
function getStartCodeBlockAboveLine(linesAboveArray) {
|
||
|
let linesAboveStr = linesAboveArray.reduce((prev, current) => {
|
||
|
return prev + "\n" + current;
|
||
|
}, "");
|
||
|
/*
|
||
|
* First thing we need to do is check if there are any end tags in the
|
||
|
* set of strings (which logically would close start tags and therefore
|
||
|
* the start tag it closes is not what we want). If there are we want to
|
||
|
* slowly narrow down our set of strings until the last end tag is
|
||
|
* removed. This makes it easier to find the closest open start tag
|
||
|
* in the data.
|
||
|
*/
|
||
|
let endTagSerachData = findEndTag(linesAboveStr);
|
||
|
while (endTagSerachData.found === true) {
|
||
|
// Get the index of where the first regex match in the
|
||
|
// string is. then we slice from 0 to index off of the string
|
||
|
// split it by newline, cut off the first line (which actually
|
||
|
// contains the regex) then reduce back down to a single string.
|
||
|
linesAboveStr = linesAboveStr.slice(endTagSerachData.endPosition);
|
||
|
endTagSerachData = findEndTag(linesAboveStr);
|
||
|
}
|
||
|
let startCodeBlockData = findStartCodeblock(linesAboveStr);
|
||
|
let codeBlockText = linesAboveStr.slice(startCodeBlockData.startPosition, startCodeBlockData.endPosition);
|
||
|
let startBlockKey = "";
|
||
|
if (startCodeBlockData.found === false) {
|
||
|
return null;
|
||
|
}
|
||
|
else {
|
||
|
/**
|
||
|
* Now we know there is at least 1 start key left, however there
|
||
|
* may be multiple start keys if the user is not closing their
|
||
|
* blocks. We currently dont allow recusive splitting so we
|
||
|
* want to get the last key in our remaining set. Same idea as
|
||
|
* above.
|
||
|
*/
|
||
|
while (startCodeBlockData.found === true) {
|
||
|
// Get the index of where the first regex match in the
|
||
|
// string is. then we slice from 0 to index off of the string
|
||
|
// split it by newline, cut off the first line (which actually
|
||
|
// contains the regex) then reduce back down to a single string.
|
||
|
codeBlockText = linesAboveStr.slice(startCodeBlockData.startPosition, startCodeBlockData.endPosition);
|
||
|
startBlockKey = parseStartRegionCodeBlockID(codeBlockText);
|
||
|
linesAboveStr = linesAboveStr.slice(startCodeBlockData.endPosition);
|
||
|
startCodeBlockData = findStartCodeblock(linesAboveStr);
|
||
|
}
|
||
|
}
|
||
|
let retLinesAboveArray = linesAboveStr.split("\n");
|
||
|
return { startBlockKey, linesAboveArray: retLinesAboveArray };
|
||
|
}
|
||
|
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 getLeafSourceMode(fileLeaf) {
|
||
|
return fileLeaf.getViewState().state.mode;
|
||
|
}
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
var ElementRenderType;
|
||
|
(function (ElementRenderType) {
|
||
|
ElementRenderType[ElementRenderType["undefined"] = 0] = "undefined";
|
||
|
ElementRenderType[ElementRenderType["normalRender"] = 1] = "normalRender";
|
||
|
ElementRenderType[ElementRenderType["specialRender"] = 2] = "specialRender";
|
||
|
ElementRenderType[ElementRenderType["specialSingleElementRender"] = 3] = "specialSingleElementRender";
|
||
|
ElementRenderType[ElementRenderType["canvasRenderElement"] = 4] = "canvasRenderElement";
|
||
|
ElementRenderType[ElementRenderType["unRendered"] = 5] = "unRendered";
|
||
|
})(ElementRenderType || (ElementRenderType = {}));
|
||
|
function getElementRenderType(element) {
|
||
|
/**
|
||
|
* 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 ElementRenderType.specialSingleElementRender;
|
||
|
}
|
||
|
/**
|
||
|
* 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 (hasDataviewJS(element) === true) {
|
||
|
return ElementRenderType.canvasRenderElement;
|
||
|
}
|
||
|
/**
|
||
|
* 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 ||
|
||
|
hasCopyButton(element) === true ||
|
||
|
hasAdmonitionFold(element) === true) {
|
||
|
return ElementRenderType.specialRender;
|
||
|
}
|
||
|
/**
|
||
|
* 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 ||
|
||
|
isIFrame(element) === true) {
|
||
|
return ElementRenderType.normalRender;
|
||
|
}
|
||
|
/**
|
||
|
* 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 ElementRenderType.normalRender;
|
||
|
}
|
||
|
// If still nothing found we return other as the default response if nothing else found.
|
||
|
return ElementRenderType.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 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("dataview").length !== 0;
|
||
|
return isDataview;
|
||
|
}
|
||
|
function hasDataviewJS(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 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) {
|
||
|
console.debug("Found multiple heading collapse indicators in element.");
|
||
|
}
|
||
|
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
|
||
|
*/
|
||
|
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 = {}));
|
||
|
class DOMObject {
|
||
|
constructor(element, linesOfElement, randomID = getUID(), tag = DOMObjectTag.none) {
|
||
|
this.clonedElement = null;
|
||
|
this.elementType = ElementRenderType.undefined;
|
||
|
this.elementContainer = null;
|
||
|
this.elementRenderedHeight = 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();
|
||
|
}
|
||
|
}
|
||
|
setMainDOMElement(domElement) {
|
||
|
this.originalElement = domElement;
|
||
|
this.usingOriginalElement = true;
|
||
|
}
|
||
|
setDomObjectTag() {
|
||
|
let elementTextSpaced = this.linesOfElement.reduce((prev, curr) => {
|
||
|
return prev + "\n" + curr;
|
||
|
});
|
||
|
if (containsEndTag(this.originalElement.textContent) === true) {
|
||
|
this.elementType = ElementRenderType.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 = ElementRenderType.unRendered;
|
||
|
this.tag = DOMObjectTag.columnBreak;
|
||
|
// el.addClass(MultiColumnStyleCSS.ColumnEndTag)
|
||
|
// regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.columnBreak);
|
||
|
}
|
||
|
else if (containsStartTag(this.originalElement.textContent) === true) {
|
||
|
this.elementType = ElementRenderType.unRendered;
|
||
|
this.tag = DOMObjectTag.startRegion;
|
||
|
// el.addClass(MultiColumnStyleCSS.ColumnEndTag)
|
||
|
// regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.columnBreak);
|
||
|
}
|
||
|
else if (containsColSettingsTag(elementTextSpaced) === true) {
|
||
|
this.elementType = ElementRenderType.unRendered;
|
||
|
// el.addClass(MultiColumnStyleCSS.RegionSettings)
|
||
|
// regionalManager = regionalContainer.setRegionSettings(elementTextSpaced)
|
||
|
// regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.regionSettings);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
class TaskListDOMObject extends DOMObject {
|
||
|
constructor(baseDOMObject) {
|
||
|
super(baseDOMObject.originalElement, baseDOMObject.linesOfElement, baseDOMObject.UID, DOMObjectTag.none);
|
||
|
this.originalCheckboxes = [];
|
||
|
}
|
||
|
checkboxClicked(index) {
|
||
|
if (index < this.originalCheckboxes.length) {
|
||
|
let originalInput = this.originalCheckboxes[index].firstChild;
|
||
|
originalInput.click();
|
||
|
}
|
||
|
}
|
||
|
static checkForTaskListElement(domElement) {
|
||
|
if (domElement.originalElement.getElementsByClassName("task-list-item").length > 0) {
|
||
|
return new TaskListDOMObject(domElement);
|
||
|
}
|
||
|
return domElement;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* 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"] = "mcm-content-overflow-auto-scroll";
|
||
|
MultiColumnLayoutCSS["ContentOverflowHidden"] = "mcm-content-overflow-hidden";
|
||
|
// ------------------------------------------------------ //
|
||
|
MultiColumnLayoutCSS["SingleColumnSmall"] = "mcm-single-column-small";
|
||
|
MultiColumnLayoutCSS["SingleColumnMed"] = "mcm-single-column-medium";
|
||
|
MultiColumnLayoutCSS["SingleColumnLarge"] = "mcm-single-column-large";
|
||
|
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 || (MultiColumnStyleCSS = {}));
|
||
|
|
||
|
/**
|
||
|
* 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 {
|
||
|
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;
|
||
|
}
|
||
|
get regionParent() {
|
||
|
return this._regionParent;
|
||
|
}
|
||
|
set regionParent(value) {
|
||
|
this._regionParent = value;
|
||
|
}
|
||
|
getRegionData() {
|
||
|
return {
|
||
|
domList: this.domList,
|
||
|
domObjectMap: this.domObjectMap,
|
||
|
regionParent: this.regionParent,
|
||
|
fileManager: this.fileManager,
|
||
|
regionalSettings: this.regionalSettings,
|
||
|
regionKey: this.regionKey,
|
||
|
rootElement: null
|
||
|
};
|
||
|
}
|
||
|
addObject(siblingsAbove, siblingsBelow, obj) {
|
||
|
let prevObj = siblingsAbove.children[siblingsAbove.children.length - 1];
|
||
|
let nextObj = siblingsBelow.children[0];
|
||
|
let addAtIndex = siblingsAbove.children.length;
|
||
|
if (prevObj !== undefined) {
|
||
|
prevObj.innerText;
|
||
|
for (let i = this.domList.length - 1; i >= 0; i--) {
|
||
|
if (this.domList[i].nodeKey === prevObj.innerText) {
|
||
|
addAtIndex = i + 1;
|
||
|
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: ", siblingsAbove.children[siblingsAbove.children.length - 1], "Adding: ", obj.element, " 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;
|
||
|
}
|
||
|
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);
|
||
|
let index = this.domList.indexOf(obj);
|
||
|
if (index !== -1) {
|
||
|
this.domList[index].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++) {
|
||
|
/**
|
||
|
* Here we check for special cases
|
||
|
*/
|
||
|
if (this.domList[i] instanceof TaskListDOMObject) {
|
||
|
this.fixClonedCheckListButtons(this.domList[i]);
|
||
|
}
|
||
|
let elementType = this.domList[i].elementType;
|
||
|
/**
|
||
|
* If the element is not currently a special render element we check again
|
||
|
* as the original element may have been updated.
|
||
|
*
|
||
|
* TODO: find a way to "Officially" mark normal elements rather than
|
||
|
* continuously search for special render types.
|
||
|
*/
|
||
|
if (elementType !== ElementRenderType.specialRender &&
|
||
|
elementType !== ElementRenderType.specialSingleElementRender &&
|
||
|
elementType !== ElementRenderType.unRendered) {
|
||
|
// 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);
|
||
|
this.domList[i].originalElement.clientHeight;
|
||
|
}
|
||
|
if (elementType === ElementRenderType.specialRender ||
|
||
|
elementType === ElementRenderType.specialSingleElementRender ||
|
||
|
elementType === ElementRenderType.canvasRenderElement) {
|
||
|
this.domList[i].elementType = elementType;
|
||
|
this.setUpDualRender(this.domList[i]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/**
|
||
|
* 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) {
|
||
|
// 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].firstChild;
|
||
|
checkbox.checked = originalInput.checked;
|
||
|
clonedListCheckboxes[i].replaceChild(checkbox, clonedListCheckboxes[i].children[0]);
|
||
|
checkbox.addClass('task-list-item-checkbox');
|
||
|
checkbox.type = 'checkbox';
|
||
|
checkbox.onClickEvent(() => {
|
||
|
domElement.checkboxClicked(i);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
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++) {
|
||
|
domElement.clonedElement.removeChild(clonedListCheckboxes[i]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
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);
|
||
|
/**
|
||
|
* 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 ||
|
||
|
clonedElementHeight !== originalElementHeight) {
|
||
|
// Update clone and reference.
|
||
|
domElement.clonedElement = 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);
|
||
|
}
|
||
|
if (domElement.elementType === ElementRenderType.canvasRenderElement) {
|
||
|
containerElement.appendChild(originalElement);
|
||
|
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) {
|
||
|
for (let i = clonedElement.children.length - 1; i >= 0; i--) {
|
||
|
clonedElement.children[i].detach();
|
||
|
}
|
||
|
clonedElement.appendChild(cloneCanvas(canvas));
|
||
|
}
|
||
|
containerElement.removeChild(originalElement);
|
||
|
}
|
||
|
/**
|
||
|
* 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 !== ElementRenderType.specialSingleElementRender) {
|
||
|
// 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 = [];
|
||
|
let styleStr = "";
|
||
|
if (settings.columnSpacing !== "") {
|
||
|
styleStr = `margin-inline: ${settings.columnSpacing};`;
|
||
|
}
|
||
|
if (settings.numberOfColumns === 2) {
|
||
|
switch (settings.columnLayout) {
|
||
|
case (ColumnLayout.standard):
|
||
|
case (ColumnLayout.middle):
|
||
|
case (ColumnLayout.center):
|
||
|
case (ColumnLayout.third):
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoEqualColumns}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoEqualColumns}`
|
||
|
}));
|
||
|
break;
|
||
|
case (ColumnLayout.left):
|
||
|
case (ColumnLayout.first):
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoColumnLarge}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoColumnSmall}`
|
||
|
}));
|
||
|
break;
|
||
|
case (ColumnLayout.right):
|
||
|
case (ColumnLayout.second):
|
||
|
case (ColumnLayout.last):
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoColumnSmall}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.TwoColumnLarge}`
|
||
|
}));
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
else if (settings.numberOfColumns === 3) {
|
||
|
switch (settings.columnLayout) {
|
||
|
case (ColumnLayout.standard):
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeEqualColumns}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeEqualColumns}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeEqualColumns}`
|
||
|
}));
|
||
|
break;
|
||
|
case (ColumnLayout.left):
|
||
|
case (ColumnLayout.first):
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Large}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
|
||
|
}));
|
||
|
break;
|
||
|
case (ColumnLayout.middle):
|
||
|
case (ColumnLayout.center):
|
||
|
case (ColumnLayout.second):
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Large}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
|
||
|
}));
|
||
|
break;
|
||
|
case (ColumnLayout.right):
|
||
|
case (ColumnLayout.third):
|
||
|
case (ColumnLayout.last):
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Small}`
|
||
|
}));
|
||
|
multiColumnParent.createDiv({
|
||
|
cls: `mcm-column-spacer`,
|
||
|
attr: { "style": styleStr }
|
||
|
});
|
||
|
columnContentDivs.push(multiColumnParent.createDiv({
|
||
|
cls: `${MultiColumnStyleCSS.ColumnContent} ${MultiColumnLayoutCSS.ThreeColumn_Large}`
|
||
|
}));
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
return columnContentDivs;
|
||
|
}
|
||
|
}
|
||
|
function getElementClientHeight(element, parentRenderElement) {
|
||
|
let height = element.clientHeight;
|
||
|
if (height === 0) {
|
||
|
parentRenderElement.appendChild(element);
|
||
|
height = element.clientHeight;
|
||
|
parentRenderElement.removeChild(element);
|
||
|
}
|
||
|
return height;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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);
|
||
|
}
|
||
|
/**
|
||
|
* 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 multiColumnParent = createDiv({
|
||
|
cls: MultiColumnLayoutCSS.RegionColumnContainerDiv,
|
||
|
});
|
||
|
/**
|
||
|
* 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 (settings.drawBorder === 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);
|
||
|
}
|
||
|
appendElementsToColumns(regionElements, columnContentDivs, settings) {
|
||
|
let columnIndex = 0;
|
||
|
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,
|
||
|
});
|
||
|
if (settings.contentOverflow === ContentOverflowType.hidden) {
|
||
|
element.addClass(MultiColumnLayoutCSS.ContentOverflowHidden);
|
||
|
}
|
||
|
else {
|
||
|
element.addClass(MultiColumnLayoutCSS.ContentOverflowAutoScroll);
|
||
|
}
|
||
|
regionElements[i].elementContainer = element;
|
||
|
// Otherwise we just make a copy of the original element to display.
|
||
|
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;
|
||
|
element.appendChild(clonedElement);
|
||
|
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++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
/**
|
||
|
* 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 multiColumnParent = createDiv({
|
||
|
cls: MultiColumnLayoutCSS.RegionColumnContainerDiv,
|
||
|
});
|
||
|
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 (settings.drawBorder === 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);
|
||
|
}
|
||
|
appendElementsToColumns(regionElements, columnContentDiv, settings) {
|
||
|
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;
|
||
|
// Otherwise we just make a copy of the original element to display.
|
||
|
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;
|
||
|
element.appendChild(clonedElement);
|
||
|
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 === SingleColumnSize.small) {
|
||
|
contentDiv.addClass(`${MultiColumnLayoutCSS.SingleColumnSmall}`);
|
||
|
}
|
||
|
else if (this.regionalSettings.columnSize === SingleColumnSize.large) {
|
||
|
contentDiv.addClass(`${MultiColumnLayoutCSS.SingleColumnLarge}`);
|
||
|
}
|
||
|
else {
|
||
|
contentDiv.addClass(`${MultiColumnLayoutCSS.SingleColumnMed}`);
|
||
|
}
|
||
|
return contentDiv;
|
||
|
}
|
||
|
}
|
||
|
function isLeftLayout(layout) {
|
||
|
if (layout === ColumnLayout.left ||
|
||
|
layout === ColumnLayout.first) {
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
function isRightLayout(layout) {
|
||
|
if (layout === ColumnLayout.right ||
|
||
|
layout === ColumnLayout.third ||
|
||
|
layout === ColumnLayout.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() {
|
||
|
super(...arguments);
|
||
|
this.previousColumnHeights = [];
|
||
|
}
|
||
|
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 multiColumnParent = createDiv({
|
||
|
cls: MultiColumnLayoutCSS.RegionColumnContainerDiv,
|
||
|
});
|
||
|
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 (settings.drawBorder === 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);
|
||
|
}
|
||
|
appendElementsToColumns(regionElements, columnContentDivs, settings) {
|
||
|
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;
|
||
|
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 &&
|
||
|
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.
|
||
|
*/
|
||
|
for (let i = 0; i < 5; 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() {
|
||
|
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/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) {
|
||
|
this.region = new StandardMultiColumnRegionManager(createDefaultRegionManagerData(regionParent, parentFileManager, regionKey, rootElement));
|
||
|
}
|
||
|
getRegion() {
|
||
|
return this.region;
|
||
|
}
|
||
|
setRegionSettings(settingsText) {
|
||
|
let regionalSettings = parseColumnSettings(settingsText);
|
||
|
if (regionalSettings.numberOfColumns === 1) {
|
||
|
regionalSettings = parseSingleColumnSettings(settingsText, 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.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;
|
||
|
}
|
||
|
}
|
||
|
function createDefaultRegionManagerData(regionParent, fileManager, regionKey, rootElement) {
|
||
|
return {
|
||
|
domList: [],
|
||
|
domObjectMap: new Map(),
|
||
|
regionParent: regionParent,
|
||
|
fileManager: fileManager,
|
||
|
regionalSettings: getDefaultMultiColumnSettings(),
|
||
|
regionKey: regionKey,
|
||
|
rootElement: rootElement
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* 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 = createFileDOMManager(this, key);
|
||
|
this.managers.set(key, fileManager);
|
||
|
}
|
||
|
return fileManager;
|
||
|
}
|
||
|
getAllFileManagers() {
|
||
|
return Array.from(this.managers.values());
|
||
|
}
|
||
|
}
|
||
|
function createFileDOMManager(parentManager, fileKey) {
|
||
|
let regionMap = new Map();
|
||
|
let hasStartTag = false;
|
||
|
function removeRegion(regionKey) {
|
||
|
let regionContainer = regionMap.get(regionKey);
|
||
|
if (regionContainer) {
|
||
|
let regionalManager = regionContainer.getRegion();
|
||
|
regionalManager.displayOriginalElements();
|
||
|
}
|
||
|
regionMap.delete(regionKey);
|
||
|
if (regionMap.size === 0) {
|
||
|
parentManager.removeFileManagerCallback(fileKey);
|
||
|
}
|
||
|
}
|
||
|
function createRegionalManager(regionKey, rootElement, errorElement, renderRegionElement) {
|
||
|
//TODO: Use the error element whenever there is an error.
|
||
|
let regonalContainer = new RegionManagerContainer(this, regionKey, rootElement, renderRegionElement);
|
||
|
regionMap.set(regionKey, regonalContainer);
|
||
|
return regonalContainer.getRegion();
|
||
|
}
|
||
|
function getRegionalContainer(regionKey) {
|
||
|
let regonalManager = null;
|
||
|
if (regionMap.has(regionKey) === true) {
|
||
|
regonalManager = regionMap.get(regionKey);
|
||
|
}
|
||
|
return regonalManager;
|
||
|
}
|
||
|
function getAllRegionalManagers() {
|
||
|
let containers = Array.from(regionMap.values());
|
||
|
let regions = containers.map((curr) => { return curr.getRegion(); });
|
||
|
return regions;
|
||
|
}
|
||
|
function setHasStartTag() {
|
||
|
hasStartTag = true;
|
||
|
}
|
||
|
function getHasStartTag() {
|
||
|
return hasStartTag;
|
||
|
}
|
||
|
function getNumberOfRegions() {
|
||
|
return regionMap.size;
|
||
|
}
|
||
|
function checkKeyExists(checkKey) {
|
||
|
return regionMap.has(checkKey);
|
||
|
}
|
||
|
return { regionMap: regionMap,
|
||
|
hasStartTag: hasStartTag,
|
||
|
createRegionalManager: createRegionalManager,
|
||
|
getRegionalContainer: getRegionalContainer,
|
||
|
getAllRegionalManagers: getAllRegionalManagers,
|
||
|
removeRegion: removeRegion,
|
||
|
setHasStartTag: setHasStartTag,
|
||
|
getHasStartTag: getHasStartTag,
|
||
|
getNumberOfRegions: getNumberOfRegions,
|
||
|
checkKeyExists: checkKeyExists
|
||
|
};
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* 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
|
||
|
*/
|
||
|
class MultiColumnMarkdown_LivePreview_Widget extends view.WidgetType {
|
||
|
constructor(contentData) {
|
||
|
super();
|
||
|
this.domList = [];
|
||
|
this.regionSettings = getDefaultMultiColumnSettings();
|
||
|
this.contentData = contentData;
|
||
|
// Find the settings defined in the content, if it exists.
|
||
|
// If the settings codeblock isnt defined attempt to get the region codeblock type.
|
||
|
let settingsStartData = findSettingsCodeblock(this.contentData);
|
||
|
if (settingsStartData.found === false) {
|
||
|
settingsStartData = findStartCodeblock(this.contentData);
|
||
|
}
|
||
|
if (settingsStartData.found === true) {
|
||
|
this.settingsText = this.contentData.slice(settingsStartData.startPosition, settingsStartData.endPosition);
|
||
|
this.contentData = this.contentData.replace(this.settingsText, "");
|
||
|
// Parse the settings, updating the default settings.
|
||
|
this.regionSettings = parseColumnSettings(this.settingsText);
|
||
|
}
|
||
|
// 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, "", elementMarkdownRenderer);
|
||
|
// 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++) {
|
||
|
this.domList.push(new DOMObject(arr[i], [""]));
|
||
|
}
|
||
|
// 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()
|
||
|
};
|
||
|
// 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);
|
||
|
}
|
||
|
else {
|
||
|
this.regionManager = new StandardMultiColumnRegionManager(regionData);
|
||
|
}
|
||
|
}
|
||
|
toDOM() {
|
||
|
// 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 leaf = null;
|
||
|
if (app) {
|
||
|
let leaves = app.workspace.getLeavesOfType("markdown");
|
||
|
if (leaves.length > 0) {
|
||
|
leaf = leaves[0];
|
||
|
}
|
||
|
}
|
||
|
if (this.regionManager) {
|
||
|
if (leaf) {
|
||
|
leaf.view.containerEl.appendChild(el);
|
||
|
}
|
||
|
this.regionManager.renderRegionElementsToLivePreview(el);
|
||
|
if (leaf) {
|
||
|
leaf.view.containerEl.removeChild(el);
|
||
|
}
|
||
|
}
|
||
|
return el;
|
||
|
}
|
||
|
}
|
||
|
class MultiColumnMarkdown_DefinedSettings_LivePreview_Widget extends view.WidgetType {
|
||
|
constructor(contentData) {
|
||
|
super();
|
||
|
this.contentData = contentData;
|
||
|
}
|
||
|
toDOM() {
|
||
|
console.log("Rendering settings block");
|
||
|
// Create our element to hold all of the live preview elements.
|
||
|
let el = document.createElement("div");
|
||
|
el.className = "mcm-cm-settings-preview";
|
||
|
let labelDiv = el.createDiv();
|
||
|
let label = labelDiv.createSpan({
|
||
|
cls: "mcm-col-settings-preview"
|
||
|
});
|
||
|
label.textContent = "Column Settings:";
|
||
|
let list = el.createEl("ul");
|
||
|
let lines = this.contentData.split("\n");
|
||
|
for (let i = 1; i < lines.length - 1; i++) {
|
||
|
let item = list.createEl("li");
|
||
|
item.textContent = lines[i];
|
||
|
}
|
||
|
return el;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* 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 generated = false;
|
||
|
language.syntaxTree(transaction.state).iterate({
|
||
|
enter(node) {
|
||
|
// 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 (generated === true) {
|
||
|
return;
|
||
|
}
|
||
|
let markdownLeaves = app.workspace.getLeavesOfType("markdown");
|
||
|
if (markdownLeaves.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
// TODO: Check other ways to get if source is live preview? editorLivePreviewField
|
||
|
if (markdownLeaves[0].getViewState().state.source === true) {
|
||
|
console.debug("User disabled live preview.");
|
||
|
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;
|
||
|
}
|
||
|
/**
|
||
|
* 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) {
|
||
|
console.debug("No start tag in document.");
|
||
|
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 startTagData = findStartTag(workingFileText);
|
||
|
if (startTagData.found === false) {
|
||
|
startTagData = findStartCodeblock(workingFileText);
|
||
|
}
|
||
|
let endTagData = findEndTag(workingFileText);
|
||
|
let loopIndex = 0;
|
||
|
let startIndexOffset = 0;
|
||
|
while (startTagData.found === true && endTagData.found === true) {
|
||
|
/**
|
||
|
* 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 + startTagData.startPosition;
|
||
|
let endIndex = startIndexOffset + 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 = docText.slice(startIndex, endIndex);
|
||
|
/**
|
||
|
* Update our start offset and the working text of the file so our next
|
||
|
* iteration knows where we left off
|
||
|
*/
|
||
|
startIndexOffset = endIndex;
|
||
|
workingFileText = docText.slice(endIndex);
|
||
|
// Here we check if the cursor is in this specific region.
|
||
|
let cursorInRegion = checkCursorInRegion(startIndex, endIndex, ranges);
|
||
|
if (cursorInRegion === true) {
|
||
|
// If the cursor is within the region we then need to know if
|
||
|
// it is within our settings block (if it exists.)
|
||
|
let settingsStartData = findStartCodeblock(elementText);
|
||
|
if (settingsStartData.found === false) {
|
||
|
settingsStartData = findSettingsCodeblock(elementText);
|
||
|
}
|
||
|
if (settingsStartData.found === true) {
|
||
|
// Since the settings block exists check if the cursor is within that region.
|
||
|
let codeblockStartIndex = startIndex + settingsStartData.startPosition;
|
||
|
let codeblockEndIndex = startIndex + settingsStartData.endPosition;
|
||
|
let settingsText = docText.slice(codeblockStartIndex, codeblockEndIndex);
|
||
|
let cursorInCodeblock = checkCursorInRegion(codeblockStartIndex, codeblockEndIndex, ranges);
|
||
|
if (cursorInCodeblock === false) {
|
||
|
// If the cursor is not within the region we pass the data to the
|
||
|
// settings view so it can be displayed in the region.
|
||
|
builder.add(codeblockStartIndex, codeblockEndIndex + 1, view.Decoration.replace({
|
||
|
widget: new MultiColumnMarkdown_DefinedSettings_LivePreview_Widget(settingsText),
|
||
|
}));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// At this point if the cursor isnt in the region we pass the data to the
|
||
|
// element to be rendered.
|
||
|
if (cursorInRegion === false) {
|
||
|
builder.add(startIndex, endIndex, view.Decoration.replace({
|
||
|
widget: new MultiColumnMarkdown_LivePreview_Widget(elementText),
|
||
|
}));
|
||
|
}
|
||
|
generated = true;
|
||
|
// ReCalculate additional start tags if there are more in document.
|
||
|
startTagData = findStartTag(workingFileText);
|
||
|
if (startTagData.found === false) {
|
||
|
startTagData = findStartCodeblock(workingFileText);
|
||
|
}
|
||
|
endTagData = findEndTag(workingFileText);
|
||
|
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) {
|
||
|
let cursorInRegion = false;
|
||
|
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) {
|
||
|
cursorInRegion = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (cursorInRegion === false && transaction.selection) {
|
||
|
for (let i = 0; i < transaction.selection.ranges.length; i++) {
|
||
|
let range = transaction.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)) {
|
||
|
cursorInRegion = true;
|
||
|
break;
|
||
|
}
|
||
|
// 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)) {
|
||
|
cursorInRegion = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return cursorInRegion;
|
||
|
}
|
||
|
},
|
||
|
provide(field) {
|
||
|
return view.EditorView.decorations.from(field);
|
||
|
},
|
||
|
});
|
||
|
|
||
|
/*
|
||
|
* 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.globalManager = new GlobalDOMManager();
|
||
|
}
|
||
|
onload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
console.log("Loading multi-column markdown");
|
||
|
this.globalManager = new GlobalDOMManager();
|
||
|
this.registerEditorExtension(multiColumnMarkdown_StateField);
|
||
|
for (let i = 0; i < CODEBLOCK_START_STRS.length; i++) {
|
||
|
let startStr = CODEBLOCK_START_STRS[i];
|
||
|
this.setupMarkdownCodeblockPostProcessor(startStr);
|
||
|
}
|
||
|
this.setupMarkdownPostProcessor();
|
||
|
//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: ID_${getUID(4)}
|
||
|
Number of Columns: 2
|
||
|
Largest Column: standard
|
||
|
\`\`\`
|
||
|
|
||
|
|
||
|
|
||
|
--- column-end ---
|
||
|
|
||
|
|
||
|
|
||
|
=== 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: `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) {
|
||
|
let startReplaceLines = (docText.slice(0, startCodeblock.startPosition).split("\n").length - 1) + lineOffset; // -1 to Zero index the replace line
|
||
|
let settingsText = docText.slice(startCodeblock.startPosition, startCodeblock.endPosition);
|
||
|
let settingsID = parseStartRegionCodeBlockID(settingsText);
|
||
|
if (settingsID === "") {
|
||
|
let replacementText = editor.getRange({ line: startReplaceLines, ch: 0 }, { line: startReplaceLines, ch: startCodeblock.matchLength }) + `\nID: ID_${getUID(4)}`;
|
||
|
editor.replaceRange(replacementText, { line: startReplaceLines, ch: 0 }, { line: startReplaceLines, ch: startCodeblock.matchLength });
|
||
|
startReplaceLines += 1; // we added a line to the doc so update our offset.
|
||
|
numCodeblocksUpdated += 1;
|
||
|
}
|
||
|
lineOffset = startReplaceLines;
|
||
|
docText = docText.slice(startCodeblock.startPosition + startCodeblock.matchLength);
|
||
|
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.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* () {
|
||
|
const sourcePath = ctx.sourcePath;
|
||
|
let fileDOMManager = this.globalManager.getFileManager(sourcePath);
|
||
|
if (fileDOMManager === null) {
|
||
|
console.log("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");
|
||
|
/**
|
||
|
* If we encounter a start tag on the document we set the flag to start
|
||
|
* parsing the rest of the document.
|
||
|
*/
|
||
|
if (containsStartTag(el.textContent) ||
|
||
|
containsStartCodeBlock(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 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);
|
||
|
//#region Depreciated Start Tag
|
||
|
/**
|
||
|
* 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(textOfElement)) {
|
||
|
/**
|
||
|
* Set up the current element to act as the parent for the
|
||
|
* multi-column region.
|
||
|
*/
|
||
|
el.children[0].detach();
|
||
|
el.classList.add(MultiColumnLayoutCSS.RegionRootContainerDiv);
|
||
|
let renderErrorRegion = el.createDiv({
|
||
|
cls: `${MultiColumnLayoutCSS.RegionErrorContainerDiv} ${MultiColumnStyleCSS.RegionErrorMessage}`,
|
||
|
});
|
||
|
let renderColumnRegion = el.createDiv({
|
||
|
cls: MultiColumnLayoutCSS.RegionContentContainerDiv
|
||
|
});
|
||
|
let startBlockData = getStartBlockAboveLine(linesOfElement);
|
||
|
if (startBlockData === null) {
|
||
|
return;
|
||
|
}
|
||
|
let regionKey = startBlockData.startBlockKey;
|
||
|
if (fileDOMManager.checkKeyExists(regionKey) === true) {
|
||
|
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 === "") {
|
||
|
renderErrorRegion.innerText = "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 {
|
||
|
renderErrorRegion.innerText = "Region ID already exists in document, please set a unique ID.\nEG: '=== multi-column-start: randomID'";
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
el.id = `MultiColumnID:${regionKey}`;
|
||
|
let elementMarkdownRenderer = new obsidian.MarkdownRenderChild(el);
|
||
|
fileDOMManager.createRegionalManager(regionKey, el, renderErrorRegion, renderColumnRegion);
|
||
|
elementMarkdownRenderer.onunload = () => {
|
||
|
if (fileDOMManager) {
|
||
|
fileDOMManager.removeRegion(startBlockData.startBlockKey);
|
||
|
}
|
||
|
};
|
||
|
ctx.addChild(elementMarkdownRenderer);
|
||
|
/**
|
||
|
* Now we have created our regional manager and defined what elements
|
||
|
* need to be rendered into. So we can return without any more processing.
|
||
|
*/
|
||
|
return;
|
||
|
}
|
||
|
//#endregion Depreciated Start Tag
|
||
|
/**
|
||
|
* 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 = getStartBlockOrCodeblockAboveLine(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.
|
||
|
*/
|
||
|
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;
|
||
|
}
|
||
|
let regionalManager = 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.
|
||
|
*/
|
||
|
linesBelowArray = getEndBlockBelow(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.
|
||
|
*/
|
||
|
let siblingsAbove = renderMarkdownFromLines(linesAboveArray, sourcePath);
|
||
|
let siblingsBelow = renderMarkdownFromLines(linesBelowArray, sourcePath);
|
||
|
/**
|
||
|
* 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 = () => {
|
||
|
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();
|
||
|
}
|
||
|
};
|
||
|
ctx.addChild(elementMarkdownRenderer);
|
||
|
let elementTextSpaced = linesOfElement.reduce((prev, curr) => {
|
||
|
return prev + "\n" + curr;
|
||
|
});
|
||
|
/**
|
||
|
* 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) {
|
||
|
currentObject.elementType = ElementRenderType.unRendered;
|
||
|
el.addClass(MultiColumnStyleCSS.RegionEndTag);
|
||
|
regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.endRegion);
|
||
|
}
|
||
|
else if (containsColEndTag(elementTextSpaced) === true) {
|
||
|
currentObject.elementType = ElementRenderType.unRendered;
|
||
|
el.addClass(MultiColumnStyleCSS.ColumnEndTag);
|
||
|
regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.columnBreak);
|
||
|
}
|
||
|
else if (containsColSettingsTag(elementTextSpaced) === true) {
|
||
|
currentObject.elementType = ElementRenderType.unRendered;
|
||
|
el.addClass(MultiColumnStyleCSS.RegionSettings);
|
||
|
regionalManager = regionalContainer.setRegionSettings(elementTextSpaced);
|
||
|
regionalManager.updateElementTag(currentObject.UID, DOMObjectTag.regionSettings);
|
||
|
}
|
||
|
else {
|
||
|
el.addClass(MultiColumnStyleCSS.RegionContent);
|
||
|
}
|
||
|
regionalManager.renderRegionElementsToScreen();
|
||
|
return;
|
||
|
}));
|
||
|
}
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
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);
|
||
|
if (blockData.isStartTag === true) {
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
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 renderErrorRegion = child.createDiv({
|
||
|
cls: `${MultiColumnLayoutCSS.RegionErrorContainerDiv}, ${MultiColumnStyleCSS.RegionErrorMessage}`,
|
||
|
});
|
||
|
let renderColumnRegion = child.createDiv({
|
||
|
cls: MultiColumnLayoutCSS.RegionContentContainerDiv
|
||
|
});
|
||
|
let regionalContainer = fileDOMManager.getRegionalContainer(regionKey);
|
||
|
if (regionalContainer === null) {
|
||
|
renderErrorRegion.innerText = "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) {
|
||
|
inBlock = false;
|
||
|
}
|
||
|
childrenToRemove.push(child);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
childrenToRemove.forEach(child => {
|
||
|
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;
|
||
|
}
|
||
|
setupMarkdownCodeblockPostProcessor(startStr) {
|
||
|
this.registerMarkdownCodeBlockProcessor(startStr, (source, el, ctx) => {
|
||
|
var _a;
|
||
|
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;
|
||
|
}
|
||
|
for (let i = 0; i < markdownLeaves.length; i++) {
|
||
|
let fileLeaf = getFileLeaf(sourcePath);
|
||
|
if (fileLeaf === null) {
|
||
|
continue;
|
||
|
}
|
||
|
if (getLeafSourceMode(fileLeaf) === "source") {
|
||
|
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;
|
||
|
}
|
||
|
// 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 renderErrorRegion = el.createDiv({
|
||
|
cls: `${MultiColumnLayoutCSS.RegionErrorContainerDiv} ${MultiColumnStyleCSS.RegionErrorMessage}`,
|
||
|
});
|
||
|
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 === "") {
|
||
|
renderErrorRegion.innerText = "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 {
|
||
|
renderErrorRegion.innerText = "Region ID already exists in document, please set a unique ID.\nEG: '=== multi-column-start: randomID'";
|
||
|
}
|
||
|
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, renderErrorRegion, 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 = el;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
module.exports = MultiColumnMarkdown;
|
||
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsibm9kZV9tb2R1bGVzL3RzbGliL3RzbGliLmVzNi5qcyIsInNyYy9yZWdpb25TZXR0aW5ncy50cyIsInNyYy91dGlsaXRpZXMvc2V0dGluZ3NQYXJzZXIudHMiLCJzcmMvdXRpbGl0aWVzL3RleHRQYXJzZXIudHMiLCJzcmMvdXRpbGl0aWVzL3V0aWxzLnRzIiwic3JjL3V0aWxpdGllcy9lbGVtZW50UmVuZGVyVHlwZVBhcnNlci50cyIsInNyYy9kb21fbWFuYWdlci9kb21PYmplY3QudHMiLCJzcmMvdXRpbGl0aWVzL2Nzc0RlZmluaXRpb25zLnRzIiwic3JjL2RvbV9tYW5hZ2VyL3JlZ2lvbmFsX21hbmFnZXJzL3JlZ2lvbk1hbmFnZXIudHMiLCJzcmMvZG9tX21hbmFnZXIvcmVnaW9uYWxfbWFuYWdlcnMvc3RhbmRhcmRNdWx0aUNvbHVtblJlZ2lvbk1hbmFnZXIudHMiLCJzcmMvZG9tX21hbmFnZXIvcmVnaW9uYWxfbWFuYWdlcnMvc2luZ2xlQ29sdW1uUmVnaW9uTWFuYWdlci50cyIsInNyYy9kb21fbWFuYWdlci9yZWdpb25hbF9tYW5hZ2Vycy9hdXRvTGF5b3V0UmVnaW9uTWFuYWdlci50cyIsInNyYy9kb21fbWFuYWdlci9yZWdpb25hbF9tYW5hZ2Vycy9yZWdpb25NYW5hZ2VyQ29udGFpbmVyLnRzIiwic3JjL2RvbV9tYW5hZ2VyL2RvbU1hbmFnZXIudHMiLCJzcmMvbGl2ZV9wcmV2aWV3L21jbV9saXZlUHJldmlld193aWRnZXQudHMiLCJzcmMvbGl2ZV9wcmV2aWV3L2NtNl9saXZlUHJldmlldy50cyIsInNyYy9tYWluLnRzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKipcclxuQ29weXJpZ2h0IChjKSBNaWNyb3NvZnQgQ29ycG9yYXRpb24uXHJcblxyXG5QZXJtaXNzaW9uIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBhbmQvb3IgZGlzdHJpYnV0ZSB0aGlzIHNvZnR3YXJlIGZvciBhbnlcclxucHVycG9zZSB3aXRoIG9yIHdpdGhvdXQgZmVlIGlzIGhlcmVieSBncmFudGVkLlxyXG5cclxuVEhFIFNPRlRXQVJFIElTIFBST1ZJREVEIFwiQVMgSVNcIiBBTkQgVEhFIEFVVEhPUiBESVNDTEFJTVMgQUxMIFdBUlJBTlRJRVMgV0lUSFxyXG5SRUdBUkQgVE8gVEhJUyBTT0ZUV0FSRSBJTkNMVURJTkcgQUxMIElNUExJRUQgV0FSUkFOVElFUyBPRiBNRVJDSEFOVEFCSUxJVFlcclxuQU5EIEZJVE5FU1MuIElOIE5PIEVWRU5UIFNIQUxMIFRIRSBBVVRIT1IgQkUgTElBQkxFIEZPUiBBTlkgU1BFQ0lBTCwgRElSRUNULFxyXG5JTkRJUkVDVCwgT1IgQ09OU0VRVUVOVElBTCBEQU1BR0VTIE9SIEFOWSBEQU1BR0VTIFdIQVRTT0VWRVIgUkVTVUxUSU5HIEZST01cclxuTE9TUyBPRiBVU0UsIERBVEEgT1IgUFJPRklUUywgV0hFVEhFUiBJTiBBTiBBQ1RJT04gT0YgQ09OVFJBQ1QsIE5FR0xJR0VOQ0UgT1JcclxuT1RIRVIgVE9SVElPVVMgQUNUSU9OLCBBUklTSU5HIE9VVCBPRiBPUiBJTiBDT05ORUNUSU9OIFdJVEggVEhFIFVTRSBPUlxyXG5QRVJGT1JNQU5DRSBPRiBUSElTIFNPRlRXQVJFLlxyXG4qKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKiAqL1xyXG4vKiBnbG9iYWwgUmVmbGVjdCwgUHJvbWlzZSAqL1xyXG5cclxudmFyIGV4dGVuZFN0YXRpY3MgPSBmdW5jdGlvbihkLCBiKSB7XHJcbiAgICBleHRlbmRTdGF0aWNzID0gT2JqZWN0LnNldFByb3RvdHlwZU9mIHx8XHJcbiAgICAgICAgKHsgX19wcm90b19fOiBbXSB9IGluc3RhbmNlb2YgQXJyYXkgJiYgZnVuY3Rpb24gKGQsIGIpIHsgZC5fX3Byb3RvX18gPSBiOyB9KSB8fFxyXG4gICAgICAgIGZ1bmN0aW9uIChkLCBiKSB7IGZvciAodmFyIHAgaW4gYikgaWYgKE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChiLCBwKSkgZFtwXSA9IGJbcF07IH07XHJcbiAgICByZXR1cm4gZXh0ZW5kU3RhdGljcyhkLCBiKTtcclxufTtcclxuXHJcbmV4cG9ydCBmdW5jdGlvbiBfX2V4dGVuZHMoZCwgYikge1xyXG4gICAgaWYgKHR5cGVvZiBiICE9PSBcImZ1bmN0aW9uXCIgJiYgYiAhPT0gbnVsbClcclxuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKFwiQ2xhc3MgZXh0ZW5kcyB2YWx1ZSBcIiArIFN0cmluZyhiKSArIFwiIGlzIG5vdCBhIGNvbnN0cnVjdG9yIG9yIG51bGxcIik7XHJcbiAgICBleHRlbmRTdGF0aWNzKGQsIGIpO1xyXG4gICAgZnVuY3Rpb24gX18oKSB7IHRoaXMuY29uc3RydWN0b3IgPSBkOyB9XHJcbiAgICBkLnByb3RvdHlwZSA9IGIgPT09IG51bGwgPyBPYmplY3QuY3JlYXRlKGIpIDogKF9fLnByb3RvdHlwZSA9IGIucHJvdG90eXBlLCBuZXcgX18oKSk7XHJcbn1cclxuXHJcbmV4cG9ydCB2YXIgX19hc3NpZ24gPSBmdW5jdGlvbigpIHtcclxuICAgIF9fYXNzaWduID0gT2JqZWN0LmFzc2lnbiB8fCBmdW5jdGlvbiBfX2Fzc2lnbih0KSB7XHJcbiAgICAgICAgZm9yICh2YXIgcywgaSA9IDEsIG4gPSBhcmd1bWVudHMubGVuZ3RoOyBpIDwgbjsgaSsrKSB7XHJcbiAgICAgICAgICAgIHMgPSBhcmd1bWVudHNbaV07XHJcbiAgICAgICAgICAgIGZvciAodmFyIHAgaW4gcykgaWYgKE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChzLCBwKSkgdFtwXSA9IHNbcF07XHJcbiAgICAgICAgfVxyXG4gICAgICAgIHJldHVybiB0O1xyXG4gICAgfVxyXG4gICAgcmV0dXJuIF9fYXNzaWduLmFwcGx5KHRoaXMsIGFyZ3VtZW50cyk7XHJcbn1cclxuXHJcbmV4cG9ydCBmdW5jdGlvbiBfX3Jlc3QocywgZSkge1xyXG4gICAgdmFyIHQgPSB7fTtcclxuICAgIGZvciAodmFyIHAgaW4gcykgaWYgKE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChzLCBwKSAmJiBlLmluZGV4T2YocCkgPCAwKVxyXG4gICAgICAgIHRbcF0gPSBzW3BdO1xyXG4gICAgaWYgKHMgIT0gbnVsbCAmJiB0eXBlb2YgT2JqZWN0LmdldE93blByb3BlcnR5U3ltYm9scyA9PT0gXCJmdW5jdGlvblwiKVxyXG4gICAgICAgIGZvciAodmFyIGkgPSAwLCBwID0gT2JqZWN0Lmd
|