/ *
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 = [ "^===\\s*?column-end\\s*?===\\s*?$" ,
"^===\\s*?end-column\\s*?===\\s*?$" ,
"^===\\s*?column-break\\s*?===\\s*?$" ,
"^===\\s*?break-column\\s*?===\\s*?$" ,
"^---\\s*?column-end\\s*?---\\s*?$" ,
"^---\\s*?end-column\\s*?---\\s*?$" ,
"^---\\s*?column-break\\s*?---\\s*?$" ,
"^---\\s*?break-column\\s*?---\\s*?$" ] ;
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 INNER _COL _END _REGEX _ARR = [
/^-{3}\s*?column-end\s*?-{3}\s*?$\n?/m ,
/^-{3}\s*?end-column\s*?-{3}\s*?$\n?/m ,
/^-{3}\s*?column-break\s*?-{3}\s*?$\n?/m ,
/^-{3}\s*?break-column\s*?-{3}\s*?$\n?/m ,
/^={3}\s*?column-end\s*?={3}\s*?$\n?/m ,
/^={3}\s*?end-column\s*?={3}\s*?$\n?/m ,
/^={3}\s*?column-break\s*?={3}\s*?$\n?/m ,
/^={3}\s*?break-column\s*?={3}\s*?$\n?/m
] ;
function checkForParagraphInnerColEndTag ( text ) {
for ( let i = 0 ; i < INNER _COL _END _REGEX _ARR . length ; i ++ ) {
let regexResult = INNER _COL _END _REGEX _ARR [ i ] . exec ( text ) ;
if ( regexResult ) {
return regexResult ;
}
}
return null ;
}
const COL _ELEMENT _INNER _TEXT _REGEX _STRS = [ "= *column-end *=" ,
"= *end-column *=" ,
"= *column-break *=" ,
"= *break-column *=" ] ;
const COL _ELEMENT _INNER _TEXT _REGEX _ARR = [ ] ;
for ( let i = 0 ; i < COL _ELEMENT _INNER _TEXT _REGEX _STRS . length ; i ++ ) {
COL _ELEMENT _INNER _TEXT _REGEX _ARR . push ( new RegExp ( COL _ELEMENT _INNER _TEXT _REGEX _STRS [ i ] ) ) ;
}
function elInnerTextContainsColEndTag ( text ) {
let found = false ;
for ( let i = 0 ; i < COL _ELEMENT _INNER _TEXT _REGEX _ARR . length ; i ++ ) {
if ( COL _ELEMENT _INNER _TEXT _REGEX _ARR [ i ] . test ( text ) ) {
found = true ;
break ;
}
}
return found ;
}
const COL _SETTINGS _REGEX _STRS = [ "```settings" ,
"```column-settings" ,
"```multi-column-settings" ] ;
const COL _SETTINGS _REGEX _ARR = [ ] ;
for ( let i = 0 ; i < COL _SETTINGS _REGEX _STRS . length ; i ++ ) {
COL _SETTINGS _REGEX _ARR . push ( new RegExp ( COL _SETTINGS _REGEX _STRS [ i ] ) ) ;
}
function containsColSettingsTag ( text ) {
let found = false ;
for ( let i = 0 ; i < COL _SETTINGS _REGEX _ARR . length ; i ++ ) {
if ( COL _SETTINGS _REGEX _ARR [ i ] . test ( text ) ) {
found = true ;
break ;
}
}
return found ;
}
function findSettingsCodeblock ( text ) {
let found = false ;
let startPosition = - 1 ;
let endPosition = - 1 ;
let matchLength = 0 ;
for ( let i = 0 ; i < COL _SETTINGS _REGEX _ARR . length ; i ++ ) {
let regexData = COL _SETTINGS _REGEX _ARR [ i ] . exec ( text ) ;
if ( regexData !== null && regexData . length > 0 ) {
found = true ;
startPosition = regexData . index ;
matchLength = regexData [ 0 ] . length ;
endPosition = startPosition + matchLength ;
let remainingText = text . slice ( endPosition ) ;
regexData = CODEBLOCK _END _REGEX . exec ( remainingText ) ;
if ( regexData !== null && regexData . length > 0 ) {
found = true ;
endPosition += regexData . index + regexData [ 0 ] . length ;
}
break ;
}
}
return { found , startPosition , endPosition , matchLength } ;
}
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 ( "block-language-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 = [ 125 , 125 , 250 , 20000 ] ;
const MID _BREAK _ERROR _MESSAGE = "Detected invalid column break syntax.\nPlease make sure column break tags are not in the middle of a paragraph block" ;
var DOMObjectTag ;
( function ( DOMObjectTag ) {
DOMObjectTag [ DOMObjectTag [ "none" ] = 0 ] = "none" ;
DOMObjectTag [ DOMObjectTag [ "startRegion" ] = 1 ] = "startRegion" ;
DOMObjectTag [ DOMObjectTag [ "regionSettings" ] = 2 ] = "regionSettings" ;
DOMObjectTag [ DOMObjectTag [ "columnBreak" ] = 3 ] = "columnBreak" ;
DOMObjectTag [ DOMObjectTag [ "endRegion" ] = 4 ] = "endRegion" ;
} ) ( DOMObjectTag || ( DOMObjectTag = { } ) ) ;
var ElementColumnBreakType ;
( function ( ElementColumnBreakType ) {
ElementColumnBreakType [ ElementColumnBreakType [ "none" ] = 0 ] = "none" ;
ElementColumnBreakType [ ElementColumnBreakType [ "preBreak" ] = 1 ] = "preBreak" ;
ElementColumnBreakType [ ElementColumnBreakType [ "postBreak" ] = 2 ] = "postBreak" ;
ElementColumnBreakType [ ElementColumnBreakType [ "midBreak" ] = 3 ] = "midBreak" ;
} ) ( ElementColumnBreakType || ( ElementColumnBreakType = { } ) ) ;
class DOMObject {
constructor ( element , linesOfElement , randomID = getUID ( ) , tag = DOMObjectTag . none ) {
this . clonedElement = null ;
this . elementIsColumnBreak = ElementColumnBreakType . none ;
this . elementType = 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 ( ) ;
}
// If our tag is still none here, we now want to check for
// an in paragraph column break flag.
if ( this . tag === DOMObjectTag . none ) {
this . checkForPrePostColumnBreak ( ) ;
}
}
setMainDOMElement ( domElement ) {
this . originalElement = domElement ;
this . usingOriginalElement = true ;
}
clonedElementReadyForUpdate ( ) {
let deltaTime = Date . now ( ) - this . lastClonedElementUpdateTime ;
if ( deltaTime > UPDATE _TIMES [ this . updateTimerIndex ] ) {
return true ;
}
return false ;
}
canvasReadyForUpdate ( ) {
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);
}
}
checkForPrePostColumnBreak ( ) {
let textOfElement = this . originalElement . innerText ;
let containsColumnBreak = checkForParagraphInnerColEndTag ( textOfElement ) ;
if ( containsColumnBreak !== null ) {
let text = this . originalElement . innerText ;
let startIndex = containsColumnBreak . index ;
let endIndex = startIndex + containsColumnBreak [ 0 ] . length ;
let pre = text . slice ( 0 , startIndex ) ;
let post = text . slice ( endIndex ) ;
let paragraph = this . originalElement . children [ 0 ] ;
if ( this . originalElement . nodeName === "P" ) {
paragraph = this . originalElement ;
}
// console.debug("Checking where column break is", startIndex, endIndex, text.length);
if ( startIndex === 0 ) {
// console.debug("column break at start of element.")
this . elementIsColumnBreak = ElementColumnBreakType . preBreak ;
}
else if ( endIndex === text . length ) {
// console.debug("Column break at end of element.")
this . elementIsColumnBreak = ElementColumnBreakType . postBreak ;
}
else {
// console.debug("Column break in the middle of element?")
this . elementIsColumnBreak = ElementColumnBreakType . midBreak ;
const ERROR _COLOR _CSS = "mcm-error-message-color" ;
const CENTER _ALIGN _SPAN _CSS = "mcm-span-content-alignment-center" ;
if ( paragraph ) {
paragraph . innerHTML = ` ${ pre } \n <span class=" ${ ERROR _COLOR _CSS } ${ CENTER _ALIGN _SPAN _CSS } "> ${ MID _BREAK _ERROR _MESSAGE } </span> \n \n ${ post } ` . split ( "\n" ) . join ( "<br>" ) ;
}
return ;
}
if ( paragraph ) {
paragraph . innerText = ` ${ pre } ${ post } ` ;
}
}
}
}
class TaskListDOMObject extends DOMObject {
constructor ( baseDOMObject ) {
super ( baseDOMObject . originalElement , baseDOMObject . linesOfElement , baseDOMObject . UID , DOMObjectTag . none ) ;
this . originalCheckboxes = [ ] ;
this . checkboxElements = new Map ( ) ;
}
checkboxClicked ( index ) {
if ( this . checkboxElements . has ( index ) ) {
this . checkboxElements . get ( index ) . click ( ) ;
}
if ( index < this . originalCheckboxes . length ) {
let originalInput = this . originalCheckboxes [ index ] . getElementsByClassName ( 'task-list-item-checkbox' ) ;
if ( originalInput . length === 1 ) {
originalInput [ 0 ] . click ( ) ;
}
else {
console . error ( "Could not find checkbox to click." ) ;
}
}
}
getCheckboxElement ( index ) {
var _a ;
if ( this . checkboxElements . has ( index ) === false ) {
if ( index < this . originalCheckboxes . length ) {
let originalInput = ( _a = this . originalCheckboxes [ index ] ) === null || _a === void 0 ? void 0 : _a . getElementsByClassName ( 'task-list-item-checkbox' ) ;
if ( ( originalInput === null || originalInput === void 0 ? void 0 : originalInput . length ) === 1 ) {
this . checkboxElements . set ( index , originalInput [ 0 ] ) ;
}
else {
console . error ( "Could not find checkbox element to return." , this . originalCheckboxes , index ) ;
}
}
}
return this . checkboxElements . get ( index ) ;
}
static checkForTaskListElement ( domElement ) {
if ( domElement . originalElement . getElementsByClassName ( "task-list-item" ) . length > 0 ) {
return new TaskListDOMObject ( domElement ) ;
}
return domElement ;
}
static getChildCheckbox ( el ) {
let checkboxElements = el . getElementsByClassName ( 'task-list-item-checkbox' ) ;
if ( checkboxElements . length === 1 ) {
return checkboxElements [ 0 ] ;
}
return el . children [ 0 ] ;
}
}
/ *
* 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 numberOfChildren ( ) {
return this . domList . length ;
}
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
} ;
}
/ * *
* Adds a new object to the region by finding where it should be relative to its siblings .
* @ param siblingsAbove The Markdown text rendered elements for sibilings above this element in the dom
* @ param siblingsBelow The Markdown text rendered elements for sibilings below this element in the dom
* @ param obj The object to add .
* @ returns Returns the index at which the object has been added .
* /
addObject ( siblingsAbove , siblingsBelow , obj ) {
let nextObj = siblingsBelow . children [ 0 ] ;
let addAtIndex = siblingsAbove . children . length ;
if ( siblingsAbove . children . length > 0 ) {
/ * *
* We want to find the first sibling withouth "" for an inner text so we can use that to anchor our
* element into the domList . For most items the first element before our new element will have the proper
* innerText . Sometimes other elements are empty and were causing issues .
*
* Now we loop back through the previous siblings looking for the first one with a valid inner text and using that
* as the anchor and offsetting our addAtIndex by the number of empty string elements we found .
* /
let prevSiblingInnerText = "" ;
let prevSiblingOffset = 0 ;
for ( let i = siblingsAbove . children . length - 1 ; i >= 0 ; i -- ) {
let obj = siblingsAbove . children [ i ] ;
if ( obj . innerText !== "" ) {
prevSiblingInnerText = obj . innerText ;
break ;
}
prevSiblingOffset ++ ;
}
for ( let i = this . domList . length - 1 ; i >= 0 ; i -- ) {
if ( this . domList [ i ] . nodeKey === prevSiblingInnerText ) {
addAtIndex = i + 1 + prevSiblingOffset ;
break ;
}
}
}
let nextElIndex = addAtIndex ;
if ( nextObj !== undefined ) {
nextObj . innerText ;
for ( let i = addAtIndex ; i < this . domList . length ; i ++ ) {
if ( this . domList [ i ] . nodeKey === nextObj . innerText . trim ( ) ) {
nextElIndex = i ;
break ;
}
}
}
// console.log(" Prev: ", Array.from(siblingsAbove.children).slice(-3), "Adding: ", obj.originalElement, " Next: ", siblingsBelow.children[0], "Overwriting:", this.domList.slice(addAtIndex, nextElIndex));
this . domList . splice ( addAtIndex , nextElIndex - addAtIndex , obj ) ;
this . domObjectMap . set ( obj . UID , obj ) ;
// /**
// * Make a copy of the list to log, only because
// * console log updates its references with updates in memory.
// */
// let x = this.domList.slice(0);
// console.log(x);
return addAtIndex ;
}
addObjectAtIndex ( obj , index ) {
this . domList . splice ( index , 0 , obj ) ;
this . domObjectMap . set ( obj . UID , obj ) ;
}
removeObject ( objectUID ) {
// /**
// * Make a copy of the list to log
// */
// let x = domList.slice(0);
// console.log(x);
// Get the object by key, remove it from the map and then
// from the list.
let obj = this . domObjectMap . get ( objectUID ) ;
this . domObjectMap . delete ( objectUID ) ;
if ( obj === undefined ) {
return ;
}
if ( this . domList . contains ( obj ) ) {
this . domList . remove ( obj ) ;
}
if ( this . domList . length === 0 && this . fileManager !== null ) {
this . fileManager . removeRegion ( this . regionKey ) ;
}
// x = domList.slice(0);
// console.log(x);
}
updateElementTag ( objectUID , newTag ) {
let obj = this . domObjectMap . get ( objectUID ) ;
obj . tag = newTag ;
}
setRegionalSettings ( regionSettings ) {
this . regionalSettings = regionSettings ;
}
/ * *
* Creates an object containing all necessary information for the region
* to be rendered to the preview pane .
*
* @ returns a MultiColumnRenderData object with the root DOM element , settings object , and
* all child objects in the order they should be rendered .
* /
getRegionRenderData ( ) {
return {
parentRenderElement : this . regionParent ,
parentRenderSettings : this . regionalSettings ,
domObjects : this . domList
} ;
}
/ * *
* This fuction is called when a start tag is removed from view meaning
* our parent element storing the multi - column region is removed . It
* removes the CSS class from all of the elements so they will be
* re - rendered in the preview window .
* /
displayOriginalElements ( ) {
for ( let i = 0 ; i < this . domList . length ; i ++ ) {
if ( this . domList [ i ] . originalElement ) {
this . domList [ i ] . originalElement . removeClasses ( [ MultiColumnStyleCSS . RegionEndTag ,
MultiColumnStyleCSS . ColumnEndTag ,
MultiColumnStyleCSS . RegionSettings ,
MultiColumnStyleCSS . RegionContent ] ) ;
if ( this . domList [ i ] . originalElement . parentElement ) {
this . domList [ i ] . originalElement . parentElement . removeChild ( this . domList [ i ] . originalElement ) ;
}
}
}
}
getID ( ) {
return this . regionKey ;
}
updateRenderedMarkdown ( ) {
/ * *
* This function acts as the update loop for the multi - column regions .
* Here we loop through all of the elements within the rendered region and
* potentially update how things are rendered . We need to do this for
* compatability with other plugins .
*
* If the multi - column region is rendered before other plugins that effect
* content within the region our rendered data may not properly display
* the content from the other plugin . Here we loop through the elements
* after all plugins have had a chance to run and can make changes to the
* DOM at this point .
* /
for ( let i = 0 ; i < this . domList . length ; i ++ ) {
/ * *
* 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 ) {
domElement . originalCheckboxes = originalListCheckboxes ;
// When we initalize we remove the old input checkbox that contains
// the weird callback situation causing the bug. Then we create a new
// checkbox to replace it and set it up to fire the click event on
// the original checkbox so functionality is restored.
for ( let i = 0 ; i < originalListCheckboxes . length ; i ++ ) {
const checkbox = createEl ( 'input' ) ;
let originalInput = domElement . getCheckboxElement ( i ) ;
checkbox . checked = originalInput === null || originalInput === void 0 ? void 0 : originalInput . checked ;
clonedListCheckboxes [ i ] . replaceChild ( checkbox , TaskListDOMObject . getChildCheckbox ( clonedListCheckboxes [ i ] ) ) ;
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.", ElementRenderType[domElement.elementType], clonedElementHeight, originalElementHeight)
// Update clone and reference.
cloneElement ( ) ;
}
if ( domElement . elementType === ElementRenderType . canvasRenderElement &&
domElement . canvasReadyForUpdate ( ) ) {
// console.log("Updating canvas re-render")
containerElement . appendChild ( originalElement ) ;
if ( clonedElement !== null && clonedElement . parentElement === containerElement ) {
containerElement . removeChild ( clonedElement ) ;
}
function cloneCanvas ( originalCanvas ) {
//create a new canvas
let clonedCanvas = originalCanvas . cloneNode ( true ) ;
let context = clonedCanvas . getContext ( '2d' ) ;
//set dimensions
clonedCanvas . width = originalCanvas . width ;
clonedCanvas . height = originalCanvas . height ;
if ( clonedCanvas . width === 0 || clonedCanvas . height === 0 ) {
// Dont want to render if the width is 0 as it throws an error
// would happen if the old canvas hasnt been rendered yet.
return clonedCanvas ;
}
//apply the old canvas to the new one
context . drawImage ( originalCanvas , 0 , 0 ) ;
//return the new canvas
return clonedCanvas ;
}
let canvas = searchChildrenForNodeType ( originalElement , "canvas" ) ;
if ( canvas !== null ) {
domElement . updateClonedElement ( originalElement . cloneNode ( true ) ) ;
clonedElement = domElement . clonedElement ;
clonedElement . addClass ( MultiColumnLayoutCSS . ClonedElementType ) ;
clonedElement . removeClasses ( [ MultiColumnStyleCSS . RegionContent , MultiColumnLayoutCSS . OriginalElementType ] ) ;
containerElement . appendChild ( clonedElement ) ;
for ( let i = clonedElement . children . length - 1 ; i >= 0 ; i -- ) {
clonedElement . children [ i ] . detach ( ) ;
}
clonedElement . appendChild ( cloneCanvas ( canvas ) ) ;
}
containerElement . removeChild ( originalElement ) ;
containerElement . appendChild ( clonedElement ) ;
}
/ * *
* 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 ) {
// If a standard element contains a column break tag and it is set as a pre content break tag we flip our index here.
if ( regionElements [ i ] . tag === DOMObjectTag . none &&
regionElements [ i ] . elementIsColumnBreak === ElementColumnBreakType . preBreak &&
( columnIndex + 1 ) < settings . numberOfColumns ) {
columnIndex ++ ;
}
// We store the elements in a wrapper container until we determine
let element = createDiv ( {
cls : MultiColumnLayoutCSS . ColumnDualElementContainer ,
} ) ;
if ( 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 ++ ;
}
else if ( regionElements [ i ] . tag === DOMObjectTag . none &&
regionElements [ i ] . elementIsColumnBreak === ElementColumnBreakType . postBreak &&
( columnIndex + 1 ) < settings . numberOfColumns ) {
// If a standard element contains a column break tag and it is set as a post content break tag we flip our index here.
columnIndex ++ ;
}
}
}
}
}
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 ;
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 ) ;
}
// 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 ;
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 ) ;
}
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 embed = null ;
let fixedEl = el ;
// image embeds can either be a <div class="internal-embed" or <p><div class="internal-embed"
// depending on the syntax this additional check is to fix false negatives when embed is
// the first case.
if ( el . hasClass ( "internal-embed" ) ) {
embed = el ;
}
else {
let items = el . getElementsByClassName ( "internal-embed" ) ;
if ( items . length !== 1 ) {
return el ;
}
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 ignoreFurtherIterations = false ;
language . syntaxTree ( transaction . state ) . iterate ( {
enter ( node ) {
// If we find that the file does not contain any MCM regions we can flip this
// flag and skip all other node iterations, potentially saving a lot of compute time.
//
// We only want to run the generation once per state change. If
// a previous node has sucessfully generated regions we ignore all
// other nodes in the state.
if ( ignoreFurtherIterations === true ) {
return ;
}
// Check if view is in live preview state.
if ( transaction . state . field ( obsidian . editorLivePreviewField ) === false ) {
// console.debug("User disabled live preview.")
ignoreFurtherIterations = true ;
return ;
}
// We want to run on the whole file so we dont just look for a single token.
const tokenProps = node . type . prop ( language . tokenClassNodeProp ) ;
if ( tokenProps !== undefined ) {
return ;
}
/ * *
* 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.")
ignoreFurtherIterations = true ;
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 ) ,
} ) ) ;
}
ignoreFurtherIterations = true ;
// Infinite loop protection.
loopIndex ++ ;
if ( loopIndex > 100 ) {
console . warn ( "Potential issue with rendering Multi-Column Markdown live preview regions. If problem persists please file a bug report with developer." ) ;
break ;
}
}
} ,
} ) ;
return builder . finish ( ) ;
function getCursorLineLocations ( ) {
let ranges = [ ] ;
if ( transaction . state . selection . ranges ) {
ranges = transaction . state . selection . ranges . filter ( ( range ) => {
return range . empty ;
} ) . map ( ( range ) => {
let line = transaction . state . doc . lineAt ( range . head ) ;
` ${ line . number } : ${ range . head - line . from } ` ;
return {
line : line ,
position : range . head
} ;
} ) ;
}
return ranges ;
}
function valueIsInRange ( value , minVal , maxVal , inclusive = true ) {
if ( inclusive === true && ( value === minVal || value === maxVal ) ) {
return true ;
}
if ( minVal < value && value < maxVal ) {
return true ;
}
return false ;
}
function checkCursorInRegion ( startIndex , endIndex , ranges ) {
for ( let i = 0 ; i < ranges . length ; i ++ ) {
// TODO: Maybe look into limiting this to the second and second to last line
// of the region as clicking right at the top or bottom of the region
// swaps it to unrendered.
let range = ranges [ i ] ;
if ( valueIsInRange ( range . position , startIndex , endIndex ) === true ) {
return true ;
}
}
if ( transaction . state . selection ) {
for ( let i = 0 ; i < transaction . state . selection . ranges . length ; i ++ ) {
let range = transaction . state . selection . ranges [ i ] ;
// If either the start or end of the selection is within the
// region range we do not render live preview.
if ( valueIsInRange ( range . from , startIndex , endIndex ) ||
valueIsInRange ( range . to , startIndex , endIndex ) ) {
return true ;
}
// // Or if the entire region is within the selection range
// we do not render the live preview.
if ( valueIsInRange ( startIndex , range . from , range . to ) &&
valueIsInRange ( endIndex , range . from , range . to ) ) {
return true ;
}
}
}
return false ;
}
} ,
provide ( field ) {
return view . EditorView . decorations . from ( field ) ;
} ,
} ) ;
/ *
* 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 ) ;
/ * *
* 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 ( textOfElement ) === true ) {
currentObject . elementType = ElementRenderType . unRendered ;
el . addClass ( MultiColumnStyleCSS . ColumnEndTag ) ;
regionalManager . updateElementTag ( currentObject . UID , DOMObjectTag . columnBreak ) ;
}
else if ( containsColSettingsTag ( textOfElement ) === true ) {
currentObject . elementType = ElementRenderType . unRendered ;
el . addClass ( MultiColumnStyleCSS . RegionSettings ) ;
regionalManager = regionalContainer . setRegionSettings ( textOfElement ) ;
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 || regionalContainer . getRegion ( ) . numberOfChildren === 0 ) {
// If the number of children is 0, we are probably in LivePreview, where the codeblock start regions have been processed by native obsidian live preview but do not have any children linked to them.
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