/ *
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 1 st 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 [ "full" ] = 3 ] = "full" ;
} ) ( SingleColumnSize || ( SingleColumnSize = { } ) ) ;
var ContentOverflowType ;
( function ( ContentOverflowType ) {
ContentOverflowType [ ContentOverflowType [ "scroll" ] = 0 ] = "scroll" ;
ContentOverflowType [ ContentOverflowType [ "hidden" ] = 1 ] = "hidden" ;
} ) ( ContentOverflowType || ( ContentOverflowType = { } ) ) ;
var AlignmentType ;
( function ( AlignmentType ) {
AlignmentType [ AlignmentType [ "left" ] = 0 ] = "left" ;
AlignmentType [ AlignmentType [ "center" ] = 1 ] = "center" ;
AlignmentType [ AlignmentType [ "right" ] = 2 ] = "right" ;
} ) ( AlignmentType || ( AlignmentType = { } ) ) ;
function getDefaultMultiColumnSettings ( ) {
return {
numberOfColumns : 2 ,
columnLayout : ColumnLayout . standard ,
drawBorder : true ,
drawShadow : true ,
autoLayout : false ,
columnSize : SingleColumnSize . medium ,
columnPosition : ColumnLayout . standard ,
columnSpacing : "" ,
contentOverflow : ContentOverflowType . scroll ,
alignment : AlignmentType . left
} ;
}
/ * *
* File : / s r c / u t i l i t i e s / s e t t i n g s P a r s e r . t s
* Created Date : Friday , June 3 rd 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" ) ;
} ) ;
const ALIGNMENT _REGEX _ARR = [
"alignment" ,
"align" ,
"content align" ,
"align content" ,
"text align" ,
"align text"
] . 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 ;
}
settingsData = getSettingsDataFromKeys ( settingsLine , ALIGNMENT _REGEX _ARR ) ;
if ( settingsData !== null ) {
let alignmentType = AlignmentType . left ;
settingsData = settingsData . toLowerCase ( ) . trim ( ) ;
if ( settingsData === "center" ) {
alignmentType = AlignmentType . center ;
}
if ( settingsData === "right" ) {
alignmentType = AlignmentType . right ;
}
parsedSettings . alignment = alignmentType ;
}
}
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 ;
case "full" :
case "full size" :
return SingleColumnSize . full ;
}
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 22 nd 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 ) {
// We want to find the first end tag in the text.
// So here we loop backwards, slicing off the tail until
// there are no more end tags available
let lastValidData = getEndTagData ( text ) ;
let workingRegexData = lastValidData ;
while ( workingRegexData . found === true ) {
lastValidData = workingRegexData ;
text = text . slice ( 0 , workingRegexData . startPosition ) ;
workingRegexData = getEndTagData ( text ) ;
}
return lastValidData ;
}
function containsEndTag ( text ) {
return findEndTag ( text ) . found ;
}
function getEndTagData ( text ) {
let found = false ;
let startPosition = - 1 ;
let endPosition = - 1 ;
let matchLength = 0 ;
for ( let i = 0 ; i < END _REGEX _ARR . length ; i ++ ) {
let regexData = END _REGEX _ARR [ i ] . exec ( text ) ;
if ( regexData !== null && regexData . length > 0 ) {
found = true ;
startPosition = regexData . index ;
matchLength = regexData [ 0 ] . length ;
break ;
}
}
endPosition = startPosition + matchLength ;
return { found , startPosition , endPosition , matchLength } ;
}
const COL _REGEX _STRS = [ "=== *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 *=" ] ;
const COL _ELEMENT _INNER _TEXT _REGEX _ARR = [ ] ;
for ( let i = 0 ; i < COL _ELEMENT _INNER _TEXT _REGEX _STRS . length ; i ++ ) {
COL _ELEMENT _INNER _TEXT _REGEX _ARR . push ( new RegExp ( COL _ELEMENT _INNER _TEXT _REGEX _STRS [ i ] ) ) ;
}
function elInnerTextContainsColEndTag ( text ) {
let found = false ;
for ( let i = 0 ; i < COL _ELEMENT _INNER _TEXT _REGEX _ARR . length ; i ++ ) {
if ( COL _ELEMENT _INNER _TEXT _REGEX _ARR [ i ] . test ( text ) ) {
found = true ;
break ;
}
}
return found ;
}
const COL _SETTINGS _REGEX _STRS = [ "```settings" ,
"```column-settings" ,
"```multi-column-settings" ] ;
const COL _SETTINGS _REGEX _ARR = [ ] ;
for ( let i = 0 ; i < COL _SETTINGS _REGEX _STRS . length ; i ++ ) {
COL _SETTINGS _REGEX _ARR . push ( new RegExp ( COL _SETTINGS _REGEX _STRS [ i ] ) ) ;
}
function containsColSettingsTag ( text ) {
let found = false ;
for ( let i = 0 ; i < COL _SETTINGS _REGEX _ARR . length ; i ++ ) {
if ( COL _SETTINGS _REGEX _ARR [ i ] . test ( text ) ) {
found = true ;
break ;
}
}
return found ;
}
function findSettingsCodeblock ( text ) {
let found = false ;
let startPosition = - 1 ;
let endPosition = - 1 ;
let matchLength = 0 ;
for ( let i = 0 ; i < COL _SETTINGS _REGEX _ARR . length ; i ++ ) {
let regexData = COL _SETTINGS _REGEX _ARR [ i ] . exec ( text ) ;
if ( regexData !== null && regexData . length > 0 ) {
found = true ;
startPosition = regexData . index ;
matchLength = regexData [ 0 ] . length ;
endPosition = startPosition + matchLength ;
let remainingText = text . slice ( endPosition ) ;
regexData = CODEBLOCK _END _REGEX . exec ( remainingText ) ;
if ( regexData !== null && regexData . length > 0 ) {
found = true ;
endPosition += regexData . index + regexData [ 0 ] . length ;
}
break ;
}
}
return { found , startPosition , endPosition , matchLength } ;
}
const CODEBLOCK _START _REGEX _STR = [
"multi-column-start" ,
"start-multi-column"
] . reduce ( ( prev , cur ) => {
if ( prev === "" ) {
return cur ;
}
return ` ${ prev } | ${ cur } ` ;
} , "" ) ;
const START _CODEBLOCK _REGEX = new RegExp ( ` \` \` \` (:? ${ CODEBLOCK _START _REGEX _STR } )(.*?) \` \` \` ` , "ms" ) ;
function findStartCodeblock ( text ) {
let found = false ;
let startPosition = - 1 ;
let endPosition = - 1 ;
let matchLength = 0 ;
let regexData = START _CODEBLOCK _REGEX . exec ( text ) ;
if ( regexData !== null && regexData . length > 0 ) {
found = true ;
startPosition = regexData . index ;
matchLength = regexData [ 0 ] . length ;
endPosition = startPosition + matchLength ;
}
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 30 th 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 ||
isInternalEmbed ( element ) ) {
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 isInternalEmbed ( element ) {
let isEmbed = element . getElementsByClassName ( "internal-embed" ) . length !== 0 ;
return isEmbed ;
}
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 1 st 2022 , 12 : 04 : 00 pm
* Author : Cameron Robinson
*
* Copyright ( c ) 2022 Cameron Robinson
* /
const UPDATE _TIMES = [ 250 , 20000 ] ;
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 . canvasElementUpdateTime = Date . now ( ) ;
this . canvasTimerIndex = 0 ;
this . lastClonedElementUpdateTime = Date . now ( ) ;
this . updateTimerIndex = 0 ;
this . nodeKey = element . innerText . trim ( ) ;
this . originalElement = element ;
this . UID = randomID ;
this . tag = tag ;
this . usingOriginalElement = false ;
this . linesOfElement = linesOfElement ;
if ( this . tag === DOMObjectTag . none ) {
this . setDomObjectTag ( ) ;
}
}
setMainDOMElement ( domElement ) {
this . originalElement = domElement ;
this . usingOriginalElement = true ;
}
clonedElementReadyForUpdate ( ) {
let deltaTime = Date . now ( ) - this . lastClonedElementUpdateTime ;
if ( deltaTime > UPDATE _TIMES [ this . updateTimerIndex ] ) {
return true ;
}
return false ;
}
canvasReadyForUpdate ( ) {
let deltaTime = Date . now ( ) - this . canvasElementUpdateTime ;
if ( deltaTime > UPDATE _TIMES [ this . canvasTimerIndex ] ) {
this . canvasElementUpdateTime = Date . now ( ) ;
this . canvasTimerIndex = Math . clamp ( this . canvasTimerIndex + 1 , 0 , UPDATE _TIMES . length - 1 ) ;
return true ;
}
return false ;
}
updateClonedElement ( newClonedElement ) {
this . clonedElement = newClonedElement ;
this . lastClonedElementUpdateTime = Date . now ( ) ;
this . updateTimerIndex = Math . clamp ( this . updateTimerIndex + 1 , 0 , UPDATE _TIMES . length - 1 ) ;
}
setDomObjectTag ( ) {
let elementTextSpaced = this . linesOfElement . reduce ( ( prev , curr ) => {
return prev + "\n" + curr ;
} ) ;
if ( containsEndTag ( this . originalElement . textContent ) === true ) {
this . elementType = 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 16 th 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 [ "AlignmentLeft" ] = "mcm-content-alignment-left" ;
MultiColumnLayoutCSS [ "AlignmentCenter" ] = "mcm-content-alignment-center" ;
MultiColumnLayoutCSS [ "AlignmentRight" ] = "mcm-content-alignment-right" ;
// ------------------------------------------------------ //
MultiColumnLayoutCSS [ "SingleColumnSmall" ] = "mcm-single-column-small" ;
MultiColumnLayoutCSS [ "SingleColumnMed" ] = "mcm-single-column-medium" ;
MultiColumnLayoutCSS [ "SingleColumnLarge" ] = "mcm-single-column-large" ;
MultiColumnLayoutCSS [ "SingleColumnFull" ] = "mcm-single-column-full" ;
MultiColumnLayoutCSS [ "SingleColumnLeftLayout" ] = "mcm-singlecol-layout-left" ;
MultiColumnLayoutCSS [ "SingleColumnCenterLayout" ] = "mcm-singlecol-layout-center" ;
MultiColumnLayoutCSS [ "SingleColumnRightLayout" ] = "mcm-singlecol-layout-right" ;
// ------------------------------------------------------ //
MultiColumnLayoutCSS [ "TwoEqualColumns" ] = "mcm-two-equal-columns" ;
MultiColumnLayoutCSS [ "TwoColumnSmall" ] = "mcm-two-column-small" ;
MultiColumnLayoutCSS [ "TwoColumnLarge" ] = "mcm-two-column-large" ;
// ------------------------------------------------------ //
MultiColumnLayoutCSS [ "ThreeEqualColumns" ] = "mcm-three-equal-columns" ;
MultiColumnLayoutCSS [ "ThreeColumn_Large" ] = "mcm-three-column-large" ;
MultiColumnLayoutCSS [ "ThreeColumn_Small" ] = "mcm-three-column-small" ;
} ) ( MultiColumnLayoutCSS || ( MultiColumnLayoutCSS = { } ) ) ;
var MultiColumnStyleCSS ;
( function ( MultiColumnStyleCSS ) {
MultiColumnStyleCSS [ "RegionErrorMessage" ] = "mcm-column-error-message" ;
MultiColumnStyleCSS [ "RegionSettings" ] = "mcm-column-settings-wrapper" ;
MultiColumnStyleCSS [ "RegionContent" ] = "mcm-column-content-wrapper" ;
MultiColumnStyleCSS [ "RegionEndTag" ] = "mcm-column-end-tag-wrapper" ;
MultiColumnStyleCSS [ "ColumnEndTag" ] = "mcm-column-break-tag-wrapper" ;
MultiColumnStyleCSS [ "RegionShadow" ] = "mcm-region-shadow" ;
MultiColumnStyleCSS [ "ColumnShadow" ] = "mcm-column-shadow" ;
MultiColumnStyleCSS [ "ColumnBorder" ] = "mcm-column-border" ;
MultiColumnStyleCSS [ "ColumnContent" ] = "mcm-column-div" ;
} ) ( MultiColumnStyleCSS || ( MultiColumnStyleCSS = { } ) ) ;
/ * *
* File : / s r c / d o m _ m a n a g e r / r e g i o n a l _ m a n a g e r s / R e g i o n M a n a g e r . t s *
* Created Date : Sunday , May 22 nd 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 nextObj = siblingsBelow . children [ 0 ] ;
let addAtIndex = siblingsAbove . children . length ;
if ( siblingsAbove . children . length > 0 ) {
/ * *
* We want to find the first sibling withouth "" for an inner text so we can use that to anchor our
* element into the domList . For most items the first element before our new element will have the proper
* innerText . Sometimes other elements are empty and were causing issues .
*
* Now we loop back through the previous siblings looking for the first one with a valid inner text and using that
* as the anchor and offsetting our addAtIndex by the number of empty string elements we found .
* /
let prevSiblingInnerText = "" ;
let prevSiblingOffset = 0 ;
for ( let i = siblingsAbove . children . length - 1 ; i >= 0 ; i -- ) {
let obj = siblingsAbove . children [ i ] ;
if ( obj . innerText !== "" ) {
prevSiblingInnerText = obj . innerText ;
break ;
}
prevSiblingOffset ++ ;
}
for ( let i = this . domList . length - 1 ; i >= 0 ; i -- ) {
if ( this . domList [ i ] . nodeKey === prevSiblingInnerText ) {
addAtIndex = i + 1 + prevSiblingOffset ;
break ;
}
}
}
let nextElIndex = addAtIndex ;
if ( nextObj !== undefined ) {
nextObj . innerText ;
for ( let i = addAtIndex ; i < this . domList . length ; i ++ ) {
if ( this . domList [ i ] . nodeKey === nextObj . innerText . trim ( ) ) {
nextElIndex = i ;
break ;
}
}
}
// console.log(" Prev: ", Array.from(siblingsAbove.children).slice(-3), "Adding: ", obj.originalElement, " Next: ", siblingsBelow.children[0], "Overwriting:", this.domList.slice(addAtIndex, nextElIndex));
this . domList . splice ( addAtIndex , nextElIndex - addAtIndex , obj ) ;
this . domObjectMap . set ( obj . UID , obj ) ;
// /**
// * Make a copy of the list to log, only because
// * console log updates its references with updates in memory.
// */
// let x = this.domList.slice(0);
// console.log(x);
return addAtIndex ;
}
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 ||
Math . abs ( clonedElementHeight - originalElementHeight ) > 10 ||
domElement . clonedElementReadyForUpdate ( ) === true ) &&
domElement . elementType !== ElementRenderType . canvasRenderElement ) {
// console.log("Updating Cloned Element.", clonedElementHeight, originalElementHeight)
// Update clone and reference.
cloneElement ( ) ;
}
if ( domElement . elementType === ElementRenderType . canvasRenderElement &&
domElement . canvasReadyForUpdate ( ) ) {
// console.log("Updating canvas re-render")
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 ) {
domElement . updateClonedElement ( originalElement . cloneNode ( true ) ) ;
clonedElement = domElement . clonedElement ;
clonedElement . addClass ( MultiColumnLayoutCSS . ClonedElementType ) ;
clonedElement . removeClasses ( [ MultiColumnStyleCSS . RegionContent , MultiColumnLayoutCSS . OriginalElementType ] ) ;
containerElement . appendChild ( clonedElement ) ;
for ( let i = clonedElement . children . length - 1 ; i >= 0 ; i -- ) {
clonedElement . children [ i ] . detach ( ) ;
}
clonedElement . appendChild ( cloneCanvas ( canvas ) ) ;
}
containerElement . removeChild ( originalElement ) ;
}
/ * *
* 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 &&
domElement . elementType !== ElementRenderType . canvasRenderElement ) {
// 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 ) ;
}
function cloneElement ( ) {
domElement . updateClonedElement ( originalElement . cloneNode ( true ) ) ;
clonedElement = domElement . clonedElement ;
/ * *
* If we updated the cloned element , we want to also update the
* element rendered in the parent container .
* /
for ( let i = containerElement . children . length - 1 ; i >= 0 ; i -- ) {
containerElement . children [ i ] . detach ( ) ;
}
// Update CSS, we add cloned class and remove classes from originalElement that do not apply.
clonedElement . addClass ( MultiColumnLayoutCSS . ClonedElementType ) ;
clonedElement . removeClasses ( [ MultiColumnStyleCSS . RegionContent , MultiColumnLayoutCSS . OriginalElementType ] ) ;
containerElement . appendChild ( clonedElement ) ;
}
}
/ * *
* 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 : / s r c / d o m _ m a n a g e r / r e g i o n a l _ m a n a g e r s / r e g i o n D O M M a n a g e r . t s *
* Created Date : Sunday , May 22 nd 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 ) ;
}
if ( settings . alignment === AlignmentType . center ) {
element . addClass ( MultiColumnLayoutCSS . AlignmentCenter ) ;
}
else if ( settings . alignment === AlignmentType . right ) {
element . addClass ( MultiColumnLayoutCSS . AlignmentRight ) ;
}
else {
element . addClass ( MultiColumnLayoutCSS . AlignmentLeft ) ;
}
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 if ( this . regionalSettings . columnSize === SingleColumnSize . full ) {
contentDiv . addClass ( ` ${ MultiColumnLayoutCSS . SingleColumnFull } ` ) ;
}
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 : / s r c / d o m _ m a n a g e r / r e g i o n a l _ m a n a g e r s / a u t o L a y o u t R e g i o n M a n a g e r . t s *
* Created Date : Sunday , May 22 nd 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 : / s r c / d o m _ m a n a g e r / r e g i o n a l _ m a n a g e r s / r e g i o n M a n a g e r C o n t a i n e r . t s *
* Created Date : Sunday , May 22 nd 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 30 th 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 16 th 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 ++ ) {
let el = this . fixElementRender ( arr [ i ] ) ;
this . domList . push ( new DOMObject ( el , [ "" ] ) ) ;
}
// 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 ) ;
}
}
fixElementRender ( el ) {
let fixedEl = fixImageRender ( el ) ;
return fixedEl ;
}
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 ) ;
}
}
fixExternalLinks ( el ) ;
return el ;
}
}
class MultiColumnMarkdown _DefinedSettings _LivePreview _Widget extends view . WidgetType {
constructor ( contentData ) {
super ( ) ;
this . contentData = contentData ;
}
toDOM ( ) {
// 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 ;
}
}
function fixImageRender ( el ) {
let fixedEl = el ;
let items = el . getElementsByClassName ( "internal-embed" ) ;
if ( items . length !== 1 ) {
return el ;
}
let embed = items [ 0 ] ;
let customWidth = embed . attributes . getNamedItem ( "width" ) ;
let alt = embed . getAttr ( "alt" ) ;
let src = embed . getAttr ( "src" ) ;
// If the link source is not an image we dont want to make any adjustments.
if ( filenameIsImage ( src ) === false ) {
return el ;
}
// Try to find the image file in the vault. This is very inefficient but works for now.
let aTFiles = app . vault . getAllLoadedFiles ( ) ;
let resourcePath = "" ;
for ( let i = 0 ; i < aTFiles . length ; i ++ ) {
let abstractFile = aTFiles [ i ] ;
if ( abstractFile instanceof obsidian . TFile === false ) {
continue ;
}
let file = abstractFile ;
if ( file . name === src && isImageExtension ( file . extension ) === true ) {
resourcePath = app . vault . getResourcePath ( file ) ;
break ;
}
}
// If we found the resource path then we update the element to be a proper image render.
if ( resourcePath !== "" ) {
fixedEl = createDiv ( {
cls : "internal-embed image-embed is-loaded" ,
} ) ;
fixedEl . setAttr ( "alt" , alt ) ;
let image = fixedEl . createEl ( "img" ) ;
image . setAttr ( "src" , resourcePath ) ;
if ( customWidth !== null ) {
image . setAttr ( "width" , customWidth . value ) ;
}
}
return fixedEl ;
}
function fixExternalLinks ( el ) {
let items = el . getElementsByClassName ( "external-link" ) ;
for ( let linkEl of Array . from ( items ) ) {
let link = linkEl ;
if ( link === undefined ||
link === null ) {
continue ;
}
// Remove the href from the link and setup an event listener to open the link in the default browser.
let href = link . getAttr ( "href" ) ;
link . removeAttribute ( "href" ) ;
link . addEventListener ( "click" , ( ev ) => {
window . open ( href ) ;
} ) ;
}
items = el . getElementsByClassName ( "internal-link" ) ;
for ( let linkEl of Array . from ( items ) ) {
let link = linkEl ;
if ( link === undefined ||
link === null ) {
continue ;
}
// Removing the href from internal links is all that seems to be required to fix the onclick.
link . removeAttribute ( "href" ) ;
}
return el ;
}
function filenameIsImage ( filename ) {
let parts = filename . split ( "." ) ;
if ( parts . length <= 1 ) {
return false ;
}
let extension = parts . last ( ) ;
return isImageExtension ( extension ) ;
}
function isImageExtension ( extension ) {
extension = extension . toLowerCase ( ) ;
switch ( extension ) {
case "png" :
case "jpg" :
case "jpeg" :
case "gif" :
case "bmp" :
case "svg" :
return true ;
}
return false ;
}
/ *
* Filename : multi - column - markdown / src / live _preview / cm6 _livePreview . ts
* Created Date : Monday , August 1 st 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 ;
}
// Check if view is in live preview state.
if ( transaction . state . field ( obsidian . editorLivePreviewField ) === false ) {
// 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 loopIndex = 0 ;
let startIndexOffset = 0 ;
while ( true ) {
// If there are multiple kinds of start blocks, the old way of parsing would cause issues.
// Now search for both kinds and determine what to do after search.
let startTagData _codeblockStart = findStartCodeblock ( workingFileText ) ;
let startTagData _depreciatedStart = findStartTag ( workingFileText ) ;
// Default to codeblock Style. Then check, if codeblock was not found and depreciated Start was, set startTag to depreciated.
let startTagData = startTagData _codeblockStart ;
if ( startTagData _codeblockStart . found === false && startTagData _depreciatedStart . found === true ) {
startTagData = startTagData _depreciatedStart ;
}
else if ( startTagData _codeblockStart . found === true && startTagData _depreciatedStart . found === true ) {
// If both kinds were found we want to start with the one closer to the top of the document as CM6 requires we work in order.
if ( startTagData _codeblockStart . startPosition > startTagData _depreciatedStart . startPosition ) {
startTagData = startTagData _depreciatedStart ;
}
}
if ( startTagData . found === false ) {
break ;
}
// Search for the first end tag after a start block. (No recursive columns.)
let endTagData = findEndTag ( workingFileText . slice ( startTagData . startPosition ) ) ;
if ( endTagData . found === false ) {
break ;
}
/ * *
* 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 = startIndex + endTagData . startPosition + endTagData . matchLength ; // Without the matchLength will leave the end tag on the screen.
// This text is the entire region data including the start and end tags.
let elementText = 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 ) ,
} ) ) ;
}
}
}
else {
// At this point if the cursor isnt in the region we pass the data to the
// element to be rendered.
builder . add ( startIndex , endIndex , view . Decoration . replace ( {
widget : new MultiColumnMarkdown _LivePreview _Widget ( elementText ) ,
} ) ) ;
}
generated = true ;
// Infinite loop protection.
loopIndex ++ ;
if ( loopIndex > 100 ) {
console . warn ( "Potential issue with rendering Multi-Column Markdown live preview regions. If problem persists please file a bug report with developer." ) ;
break ;
}
}
} ,
} ) ;
return builder . finish ( ) ;
function getCursorLineLocations ( ) {
let ranges = [ ] ;
if ( transaction . state . selection . ranges ) {
ranges = transaction . state . selection . ranges . filter ( ( range ) => {
return range . empty ;
} ) . map ( ( range ) => {
let line = transaction . state . doc . lineAt ( range . head ) ;
` ${ line . number } : ${ range . head - line . from } ` ;
return {
line : line ,
position : range . head
} ;
} ) ;
}
return ranges ;
}
function valueIsInRange ( value , minVal , maxVal , inclusive = true ) {
if ( inclusive === true && ( value === minVal || value === maxVal ) ) {
return true ;
}
if ( minVal < value && value < maxVal ) {
return true ;
}
return false ;
}
function checkCursorInRegion ( startIndex , endIndex , ranges ) {
for ( let i = 0 ; i < ranges . length ; i ++ ) {
// TODO: Maybe look into limiting this to the second and second to last line
// of the region as clicking right at the top or bottom of the region
// swaps it to unrendered.
let range = ranges [ i ] ;
if ( valueIsInRange ( range . position , startIndex , endIndex ) === true ) {
return true ;
}
}
if ( transaction . state . selection ) {
for ( let i = 0 ; i < transaction . state . selection . ranges . length ; i ++ ) {
let range = transaction . state . selection . ranges [ i ] ;
// If either the start or end of the selection is within the
// region range we do not render live preview.
if ( valueIsInRange ( range . from , startIndex , endIndex ) ||
valueIsInRange ( range . to , startIndex , endIndex ) ) {
return true ;
}
// // Or if the entire region is within the selection range
// we do not render the live preview.
if ( valueIsInRange ( startIndex , range . from , range . to ) &&
valueIsInRange ( endIndex , range . from , range . to ) ) {
return true ;
}
}
}
return false ;
}
} ,
provide ( field ) {
return view . EditorView . decorations . from ( field ) ;
} ,
} ) ;
/ *
* File : multi - column - markdown / src / main . ts
* Created Date : Tuesday , October 5 th 2021 , 1 : 09 pm
* Author : Cameron Robinson
*
* Copyright ( c ) 2022 Cameron Robinson
* /
const DEFAULT _SETTINGS = {
renderOnMobile : true
} ;
const CODEBLOCK _START _STRS = [
"start-multi-column" ,
"multi-column-start"
] ;
class MultiColumnMarkdown extends obsidian . Plugin {
constructor ( ) {
super ( ... arguments ) ;
this . settings = DEFAULT _SETTINGS ;
this . globalManager = new GlobalDOMManager ( ) ;
}
onload ( ) {
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
console . log ( "Loading multi-column markdown" ) ;
yield this . loadSettings ( ) ;
this . globalManager = new GlobalDOMManager ( ) ;
this . registerEditorExtension ( multiColumnMarkdown _StateField ) ;
for ( let i = 0 ; i < CODEBLOCK _START _STRS . length ; i ++ ) {
let startStr = CODEBLOCK _START _STRS [ i ] ;
this . setupMarkdownCodeblockPostProcessor ( startStr ) ;
}
this . setupMarkdownPostProcessor ( ) ;
this . addCommand ( {
id : ` toggle-mobile-rendering-mcm ` ,
name : ` Toggle Mobile Rendering - Multi-Column Markdown ` ,
callback : ( ) => _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . settings . renderOnMobile = ! this . settings . renderOnMobile ;
yield this . saveSettings ( ) ;
console . log ( "render on mobile:" , this . settings . renderOnMobile ) ;
let noticeString = ` Toggled mobile rendering ${ this . settings . renderOnMobile ? "on" : "off" } . ` ;
if ( obsidian . Platform . isMobile === true ) {
noticeString += ` Please reload any open files for change to take effect. ` ;
}
new obsidian . Notice ( noticeString ) ;
} )
} ) ;
//TODO: Set up this as a modal to set settings automatically
this . addCommand ( {
id : ` insert-multi-column-region ` ,
name : ` Insert Multi-Column Region ` ,
editorCallback : ( editor , view ) => {
try {
let cursorStartPosition = editor . getCursor ( "from" ) ;
editor . getDoc ( ) . replaceSelection ( `
\ ` \` \` start-multi-column
ID : 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 ) {
// Get the text of the settings block so we can check if it contains an ID,
// also so we can get the length of the first line, used to calculate where to append a new ID if needed
let settingsText = docText . slice ( startCodeblock . startPosition , startCodeblock . endPosition ) ;
let firstLineOfCodeblockLength = settingsText . split ( "\n" ) [ 0 ] . length ;
// We need the lines before the block to know where to start replacing text
// and the lines including the block to know where to set our offset to after this iteration.
let linesBefore = docText . slice ( 0 , startCodeblock . startPosition ) ;
let startReplacementLineIndex = ( linesBefore . split ( "\n" ) . length - 1 ) + lineOffset ;
let linesOf = docText . slice ( 0 , startCodeblock . endPosition ) ;
let endReplacementLineIndex = ( linesOf . split ( "\n" ) . length - 1 ) + lineOffset ;
let settingsID = parseStartRegionCodeBlockID ( settingsText ) ;
if ( settingsID === "" ) {
// copy the first line of the codeblock and append a new ID, then replace the first line of the block
let replacementText = editor . getRange ( { line : startReplacementLineIndex , ch : 0 } , { line : startReplacementLineIndex , ch : firstLineOfCodeblockLength } ) + ` \n ID: ID_ ${ getUID ( 4 ) } ` ;
editor . replaceRange ( replacementText , { line : startReplacementLineIndex , ch : 0 } , { line : startReplacementLineIndex , ch : firstLineOfCodeblockLength } ) ;
endReplacementLineIndex += 1 ;
numCodeblocksUpdated += 1 ;
}
lineOffset = endReplacementLineIndex ;
docText = docText . slice ( startCodeblock . endPosition ) ;
startCodeblock = findStartCodeblock ( docText ) ;
}
/ * *
* Loop through all of the lines checking if the line is a
* start tag and if so is it missing an ID .
* /
let linesWithoutIDs = [ ] ;
let textWithoutIDs = [ ] ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
let data = isStartTagWithID ( lines [ i ] ) ;
if ( data . isStartTag === true && data . hasKey === false ) {
linesWithoutIDs . push ( i ) ;
textWithoutIDs . push ( lines [ i ] ) ;
}
}
if ( linesWithoutIDs . length === 0 && numCodeblocksUpdated === 0 ) {
new obsidian . Notice ( "Found 0 missing IDs in the current document." ) ;
return ;
}
/ * *
* Now loop through each line that is missing an ID and
* generate a random ID and replace the original text .
* /
for ( let i = 0 ; i < linesWithoutIDs . length ; i ++ ) {
let originalText = textWithoutIDs [ i ] ;
let text = originalText ;
text = text . trimEnd ( ) ;
if ( text . charAt ( text . length - 1 ) === ":" ) {
text = text . slice ( 0 , text . length - 1 ) ;
}
text = ` ${ text } : ID_ ${ getUID ( 4 ) } ` ;
editor . replaceRange ( text , { line : linesWithoutIDs [ i ] , ch : 0 } , { line : linesWithoutIDs [ i ] , ch : originalText . length } ) ;
}
new obsidian . Notice ( ` Replaced ${ linesWithoutIDs . length + numCodeblocksUpdated } missing ID(s) in the current document. ` ) ;
}
catch ( e ) {
new obsidian . Notice ( "Encountered an error addign IDs to multi-column regions. Please try again later." ) ;
}
}
} ) ;
this . registerInterval ( window . setInterval ( ( ) => {
this . UpdateOpenFilePreviews ( ) ;
} , 500 ) ) ;
} ) ;
}
UpdateOpenFilePreviews ( ) {
let fileManagers = this . globalManager . getAllFileManagers ( ) ;
fileManagers . forEach ( element => {
let regionalManagers = element . getAllRegionalManagers ( ) ;
regionalManagers . forEach ( regionManager => {
regionManager . updateRenderedMarkdown ( ) ;
} ) ;
} ) ;
}
setupMarkdownPostProcessor ( ) {
this . registerMarkdownPostProcessor ( ( el , ctx ) => _ _awaiter ( this , void 0 , void 0 , function * ( ) {
if ( this . settings . renderOnMobile === false &&
obsidian . Platform . isMobile === true ) {
return ;
}
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 ) ;
}
}
else {
console . error ( ` Error getting file from source path: ${ sourcePath } ` ) ;
}
let inBlock = false ;
for ( let i = 0 ; i < docChildren . length ; i ++ ) {
let child = docChildren [ i ] ;
if ( child instanceof HTMLElement ) {
if ( inBlock === false ) {
let foundBlockData = false ;
let regionKey = "" ;
let blockData = isStartTagWithID ( child . textContent ) ;
if ( blockData . isStartTag === true ) {
// If an old-style start tag.
foundBlockData = true ;
if ( blockData . hasKey === true ) {
let foundKey = getStartTagKey ( child . textContent ) ;
if ( foundKey !== null ) {
regionKey = foundKey ;
}
}
}
else if ( blockData . isStartTag === false && this . isStartCodeblockInExport ( child ) ) {
// If the start tag from the old version is null we then check to see if the element is
// a codeblock start. If it is we use the next available codeblock data to retrieve our ID.
let codeblockText = codeblockStartBlocks . shift ( ) ;
if ( codeblockText === undefined ) {
console . error ( "Found undefined codeblock data when exporting." ) ;
return ;
}
let id = parseStartRegionCodeBlockID ( codeblockText ) ;
if ( id !== "" ) {
foundBlockData = true ;
regionKey = id ;
}
}
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 => {
if ( child . parentElement === el ) {
el . removeChild ( child ) ;
}
} ) ;
} ) ;
}
checkExporting ( element ) {
if ( element === null ) {
return false ;
}
if ( element . classList . contains ( "print" ) ) {
return true ;
}
if ( element . parentNode !== null ) {
return this . checkExporting ( element . parentElement ) ;
}
return false ;
}
setupMarkdownCodeblockPostProcessor ( startStr ) {
this . registerMarkdownCodeBlockProcessor ( startStr , ( source , el , ctx ) => {
var _a ;
if ( this . settings . renderOnMobile === false &&
obsidian . Platform . isMobile === true ) {
return ;
}
const sourcePath = ctx . sourcePath ;
// Set up our CSS so that the codeblock only renders this data in reading mode
// source/live preview mode is handled by the CM6 implementation.
( _a = el . parentElement ) === null || _a === void 0 ? void 0 : _a . addClass ( "preivew-mcm-start-block" ) ;
// To determine what kind of view we are rendering in we need a markdown leaf.
// Really this should never return here since rendering is only done in markdown leaves.
let markdownLeaves = app . workspace . getLeavesOfType ( "markdown" ) ;
if ( markdownLeaves . length === 0 ) {
return ;
}
for ( let i = 0 ; i < markdownLeaves . length ; i ++ ) {
let fileLeaf = getFileLeaf ( sourcePath ) ;
if ( fileLeaf === null ) {
continue ;
}
if ( getLeafSourceMode ( fileLeaf ) === "source" ) {
// This was added when implementing live preview, but the reason it was originally added appears to no longer be an issue.
// Removing the return from here to fix bug where opening multiple copies of the document causes regions to not render.
console . debug ( "Leaf of file is in source mode. Should we be ignoring post processing here?" ) ;
// 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 ;
}
} ) ;
}
loadSettings ( ) {
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
this . settings = Object . assign ( { } , DEFAULT _SETTINGS , yield this . loadData ( ) ) ;
} ) ;
}
saveSettings ( ) {
return _ _awaiter ( this , void 0 , void 0 , function * ( ) {
yield this . saveData ( this . settings ) ;
} ) ;
}
}
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