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

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