2023-05-31 18:32:33 +08:00
/ * *
* Widget : Show highlighted text in the right pane
*
2023-06-04 17:46:37 +02:00
* By design , there ' s no support for nonsensical or malformed constructs :
2023-05-31 18:32:33 +08:00
* - For example , if there is a formula in the middle of the highlighted text , the two ends of the formula will be regarded as two entries
* /
import attributeService from "../services/attributes.js" ;
import RightPanelWidget from "./right_panel_widget.js" ;
import options from "../services/options.js" ;
import OnClickButtonWidget from "./buttons/onclick_button.js" ;
2023-11-03 10:44:14 +01:00
import appContext from "../components/app_context.js" ;
2024-09-12 09:36:08 +08:00
import libraryLoader from "../services/library_loader.js" ;
2023-05-31 18:32:33 +08:00
2023-06-22 15:38:36 +08:00
const TPL = ` <div class="highlights-list-widget">
2023-05-31 18:32:33 +08:00
< style >
2023-06-22 15:38:36 +08:00
. highlights - list - widget {
2023-05-31 18:32:33 +08:00
padding : 10 px ;
contain : none ;
overflow : auto ;
position : relative ;
}
2023-06-22 15:38:36 +08:00
. highlights - list > ol {
2023-05-31 18:32:33 +08:00
padding - left : 20 px ;
}
2023-06-22 15:38:36 +08:00
. highlights - list li {
2023-05-31 18:32:33 +08:00
cursor : pointer ;
margin - bottom : 3 px ;
text - align : justify ;
2023-06-03 14:43:20 +08:00
word - wrap : break - word ;
hyphens : auto ;
2023-05-31 18:32:33 +08:00
}
2023-06-22 15:38:36 +08:00
. highlights - list li : hover {
2023-05-31 18:32:33 +08:00
font - weight : bold ;
}
< / s t y l e >
2023-06-22 15:38:36 +08:00
< span class = "highlights-list" > < / s p a n >
2023-05-31 18:32:33 +08:00
< / d i v > ` ;
2023-06-04 17:46:37 +02:00
export default class HighlightsListWidget extends RightPanelWidget {
2023-05-31 18:32:33 +08:00
get widgetTitle ( ) {
2023-06-22 15:38:36 +08:00
return "Highlights List" ;
2023-05-31 18:32:33 +08:00
}
2023-11-03 10:44:14 +01:00
get widgetButtons ( ) {
return [
new OnClickButtonWidget ( )
2024-09-09 20:30:35 +03:00
. icon ( "bx-cog" )
2023-11-03 10:44:14 +01:00
. title ( "Options" )
. titlePlacement ( "left" )
2024-09-12 09:36:08 +08:00
. onClick ( ( ) => appContext . tabManager . openContextWithNote ( '_optionsTextNotes' , { activate : true } ) )
2023-11-03 10:44:14 +01:00
. class ( "icon-action" ) ,
new OnClickButtonWidget ( )
. icon ( "bx-x" )
. titlePlacement ( "left" )
. onClick ( widget => widget . triggerCommand ( "closeHlt" ) )
. class ( "icon-action" )
] ;
}
2023-05-31 18:32:33 +08:00
isEnabled ( ) {
return super . isEnabled ( )
&& this . note . type === 'text'
2023-06-22 15:38:36 +08:00
&& ! this . noteContext . viewScope . highlightsListTemporarilyHidden
2023-05-31 18:32:33 +08:00
&& this . noteContext . viewScope . viewMode === 'default' ;
}
async doRenderBody ( ) {
this . $body . empty ( ) . append ( $ ( TPL ) ) ;
2023-06-22 15:38:36 +08:00
this . $highlightsList = this . $body . find ( '.highlights-list' ) ;
2023-05-31 18:32:33 +08:00
}
async refreshWithNote ( note ) {
2023-06-22 15:38:36 +08:00
/ * T h e r e a s o n f o r a d d i n g h i g h l i g h t s L i s t P r e v i o u s V i s i b l e i s t o r e c o r d w h e t h e r t h e p r e v i o u s s t a t e
of the highlightsList is hidden or displayed , and then let it be displayed / hidden at the initial time .
2023-06-04 17:46:37 +02:00
If there is no such value , when the right panel needs to display toc but not highlighttext ,
every time the note content is changed , highlighttext Widget will appear and then close immediately ,
because getHlt function will consume time * /
2023-06-22 15:38:36 +08:00
if ( this . noteContext . viewScope . highlightsListPreviousVisible ) {
2023-06-01 20:17:00 +08:00
this . toggleInt ( true ) ;
} else {
this . toggleInt ( false ) ;
}
2023-05-31 18:32:33 +08:00
2023-06-22 15:38:36 +08:00
const optionsHighlightsList = JSON . parse ( options . get ( 'highlightsList' ) ) ;
2023-06-01 20:38:55 +08:00
2023-07-22 19:13:03 +00:00
if ( note . isLabelTruthy ( 'hideHighlightWidget' ) || ! optionsHighlightsList . length ) {
2023-05-31 18:32:33 +08:00
this . toggleInt ( false ) ;
this . triggerCommand ( "reEvaluateRightPaneVisibility" ) ;
return ;
}
2023-06-22 15:38:36 +08:00
let $highlightsList = "" , hlLiCount = - 1 ;
2023-05-31 18:32:33 +08:00
// Check for type text unconditionally in case alwaysShowWidget is set
if ( this . note . type === 'text' ) {
2024-09-12 09:36:08 +08:00
const { content } = await note . getNoteComplement ( ) ;
( { $highlightsList , hlLiCount } = await this . getHighlightList ( content , optionsHighlightsList ) ) ;
2023-05-31 18:32:33 +08:00
}
2023-06-04 17:46:37 +02:00
this . $highlightsList . empty ( ) . append ( $highlightsList ) ;
2023-06-22 15:38:36 +08:00
if ( hlLiCount > 0 ) {
2023-06-01 20:17:00 +08:00
this . toggleInt ( true ) ;
2023-06-22 15:38:36 +08:00
this . noteContext . viewScope . highlightsListPreviousVisible = true ;
2023-06-01 20:17:00 +08:00
} else {
this . toggleInt ( false ) ;
2023-06-22 15:38:36 +08:00
this . noteContext . viewScope . highlightsListPreviousVisible = false ;
2023-05-31 18:32:33 +08:00
}
2023-06-01 20:17:00 +08:00
this . triggerCommand ( "reEvaluateRightPaneVisibility" ) ;
2023-05-31 18:32:33 +08:00
}
2023-06-01 20:17:00 +08:00
2024-09-12 09:36:08 +08:00
extractOuterTag ( htmlStr ) {
if ( htmlStr === null ) {
return null
}
// Regular expressions that match only the outermost tag
const regex = /^<([a-zA-Z]+)([^>]*)>/ ;
const match = htmlStr . match ( regex ) ;
if ( match ) {
const tagName = match [ 1 ] . toLowerCase ( ) ; // Extract tag name
const attributes = match [ 2 ] . trim ( ) ; // Extract label attributes
return { tagName , attributes } ;
}
return null ;
}
areOuterTagsConsistent ( str1 , str2 ) {
const tag1 = this . extractOuterTag ( str1 ) ;
const tag2 = this . extractOuterTag ( str2 ) ;
// If one of them has no label, returns false
if ( ! tag1 || ! tag2 ) {
return false ;
}
// Compare tag names and attributes to see if they are the same
return tag1 . tagName === tag2 . tagName && tag1 . attributes === tag2 . attributes ;
}
/ * *
* Rendering formulas in strings using katex
*
* @ param { string } html Note ' s html content
* @ returns { string } The HTML content with mathematical formulas rendered by KaTeX .
* /
async replaceMathTextWithKatax ( html ) {
const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g ;
var matches = [ ... html . matchAll ( mathTextRegex ) ] ;
let modifiedText = html ;
if ( matches . length > 0 ) {
// Process all matches asynchronously
for ( const match of matches ) {
let latexCode = match [ 1 ] ;
let rendered ;
try {
rendered = katex . renderToString ( latexCode , {
throwOnError : false
} ) ;
} catch ( e ) {
if ( e instanceof ReferenceError && e . message . includes ( 'katex is not defined' ) ) {
// Load KaTeX if it is not already loaded
await libraryLoader . requireLibrary ( libraryLoader . KATEX ) ;
try {
rendered = katex . renderToString ( latexCode , {
throwOnError : false
} ) ;
} catch ( renderError ) {
console . error ( "KaTeX rendering error after loading library:" , renderError ) ;
rendered = match [ 0 ] ; // Fall back to original if error persists
}
} else {
console . error ( "KaTeX rendering error:" , e ) ;
rendered = match [ 0 ] ; // Fall back to original on error
}
}
// Replace the matched formula in the modified text
modifiedText = modifiedText . replace ( match [ 0 ] , rendered ) ;
}
}
return modifiedText ;
}
async getHighlightList ( content , optionsHighlightsList ) {
2023-06-03 11:55:50 +08:00
// matches a span containing background-color
2023-06-04 17:46:37 +02:00
const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi ;
2023-06-03 11:55:50 +08:00
// matches a span containing color
const regex2 = /<span[^>]*style\s*=\s*[^>]*[^-]color:[^>]*?>[\s\S]*?<\/span>/gi ;
// match italics
const regex3 = /<i>[\s\S]*?<\/i>/gi ;
// match bold
const regex4 = /<strong>[\s\S]*?<\/strong>/gi ;
// match underline
const regex5 = /<u>[\s\S]*?<\/u>/g ;
2023-06-22 15:38:36 +08:00
// Possible values in optionsHighlightsList: '["bold","italic","underline","color","bgColor"]'
2023-06-04 16:02:30 +08:00
// element priority: span>i>strong>u
2023-06-04 17:46:37 +02:00
let findSubStr = "" , combinedRegexStr = "" ;
2023-06-22 15:38:36 +08:00
if ( optionsHighlightsList . includes ( "bgColor" ) ) {
findSubStr += ` ,span[style*="background-color"]:not(section.include-note span[style*="background-color"]) ` ;
2023-06-04 17:46:37 +02:00
combinedRegexStr += ` | ${ regex1 . source } ` ;
2023-06-03 11:55:50 +08:00
}
2023-06-22 15:38:36 +08:00
if ( optionsHighlightsList . includes ( "color" ) ) {
findSubStr += ` ,span[style*="color"]:not(section.include-note span[style*="color"]) ` ;
2023-06-04 17:46:37 +02:00
combinedRegexStr += ` | ${ regex2 . source } ` ;
2023-06-03 11:55:50 +08:00
}
2023-06-22 15:38:36 +08:00
if ( optionsHighlightsList . includes ( "italic" ) ) {
findSubStr += ` ,i:not(section.include-note i) ` ;
2023-06-04 17:46:37 +02:00
combinedRegexStr += ` | ${ regex3 . source } ` ;
2023-06-03 11:55:50 +08:00
}
2023-06-22 15:38:36 +08:00
if ( optionsHighlightsList . includes ( "bold" ) ) {
findSubStr += ` ,strong:not(section.include-note strong) ` ;
2023-06-04 17:46:37 +02:00
combinedRegexStr += ` | ${ regex4 . source } ` ;
2023-06-03 11:55:50 +08:00
}
2023-06-22 15:38:36 +08:00
if ( optionsHighlightsList . includes ( "underline" ) ) {
findSubStr += ` ,u:not(section.include-note u) ` ;
2023-06-04 17:46:37 +02:00
combinedRegexStr += ` | ${ regex5 . source } ` ;
2023-06-03 11:55:50 +08:00
}
findSubStr = findSubStr . substring ( 1 )
combinedRegexStr = ` ( ` + combinedRegexStr . substring ( 1 ) + ` ) ` ;
const combinedRegex = new RegExp ( combinedRegexStr , 'gi' ) ;
2023-06-04 17:46:37 +02:00
const $highlightsList = $ ( "<ol>" ) ;
2023-06-22 15:38:36 +08:00
let prevEndIndex = - 1 , hlLiCount = 0 ;
2024-09-12 09:36:08 +08:00
let prevSubHtml = null ;
// Used to determine if a string is only a formula
const onlyMathRegex = /^<span class="math-tex">\\\([^\)]*?\)<\/span>(?:<span class="math-tex">\\\([^\)]*?\)<\/span>)*$/ ;
2023-06-04 17:46:37 +02:00
for ( let match = null , hltIndex = 0 ; ( ( match = combinedRegex . exec ( content ) ) !== null ) ; hltIndex ++ ) {
const subHtml = match [ 0 ] ;
2023-06-01 20:17:00 +08:00
const startIndex = match . index ;
2023-06-03 11:55:50 +08:00
const endIndex = combinedRegex . lastIndex ;
2023-06-04 17:46:37 +02:00
if ( prevEndIndex !== - 1 && startIndex === prevEndIndex ) {
// If the previous element is connected to this element in HTML, then concatenate them into one.
$highlightsList . children ( ) . last ( ) . append ( subHtml ) ;
2023-06-03 11:55:50 +08:00
} else {
2023-06-04 17:46:37 +02:00
// TODO: can't be done with $(subHtml).text()?
2023-06-22 15:38:36 +08:00
//Can’ t remember why regular expressions are used here, but modified to $(subHtml).text() works as expected
//const hasText = [...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim();
const hasText = $ ( subHtml ) . text ( ) . trim ( ) ;
2023-06-04 17:46:37 +02:00
if ( hasText ) {
2024-09-12 09:36:08 +08:00
const substring = content . substring ( prevEndIndex , startIndex ) ;
//If the two elements have the same style and there are only formulas in between, append the formulas and the current element to the end of the previous element.
if ( this . areOuterTagsConsistent ( prevSubHtml , subHtml ) && onlyMathRegex . test ( substring ) ) {
const $lastLi = $highlightsList . children ( 'li' ) . last ( ) ;
$lastLi . append ( await this . replaceMathTextWithKatax ( substring ) ) ;
$lastLi . append ( subHtml ) ;
} else {
$highlightsList . append (
$ ( '<li>' )
. html ( subHtml )
. on ( "click" , ( ) => this . jumpToHighlightsList ( findSubStr , hltIndex ) )
) ;
}
2023-06-04 17:46:37 +02:00
2023-06-22 15:38:36 +08:00
hlLiCount ++ ;
2023-06-04 17:46:37 +02:00
} else {
// hide li if its text content is empty
continue ;
2023-06-03 14:43:20 +08:00
}
2023-05-31 18:32:33 +08:00
}
2023-06-01 20:17:00 +08:00
prevEndIndex = endIndex ;
2024-09-12 09:36:08 +08:00
prevSubHtml = subHtml ;
2023-06-01 20:17:00 +08:00
}
2023-05-31 18:32:33 +08:00
return {
2023-06-04 17:46:37 +02:00
$highlightsList ,
2023-06-22 15:38:36 +08:00
hlLiCount
2023-05-31 18:32:33 +08:00
} ;
}
2023-06-04 17:46:37 +02:00
2023-06-22 15:38:36 +08:00
async jumpToHighlightsList ( findSubStr , itemIndex ) {
2023-05-31 18:32:33 +08:00
const isReadOnly = await this . noteContext . isReadOnly ( ) ;
2023-06-03 11:55:50 +08:00
let targetElement ;
2023-05-31 18:32:33 +08:00
if ( isReadOnly ) {
const $container = await this . noteContext . getContentElement ( ) ;
2023-06-04 17:46:37 +02:00
targetElement = $container . find ( findSubStr ) . filter ( function ( ) {
if ( findSubStr . indexOf ( "color" ) >= 0 && findSubStr . indexOf ( "background-color" ) < 0 ) {
2023-06-03 11:55:50 +08:00
let color = this . style . color ;
2023-06-04 17:46:37 +02:00
return ! ( $ ( this ) . prop ( 'tagName' ) === "SPAN" && color === "" ) ;
} else {
2023-06-03 11:55:50 +08:00
return true ;
2023-06-04 17:46:37 +02:00
}
} ) . filter ( function ( ) {
return $ ( this ) . parent ( findSubStr ) . length === 0
&& $ ( this ) . parent ( ) . parent ( findSubStr ) . length === 0
&& $ ( this ) . parent ( ) . parent ( ) . parent ( findSubStr ) . length === 0
&& $ ( this ) . parent ( ) . parent ( ) . parent ( ) . parent ( findSubStr ) . length === 0 ;
2023-06-03 11:55:50 +08:00
} )
2023-05-31 18:32:33 +08:00
} else {
const textEditor = await this . noteContext . getTextEditor ( ) ;
2023-06-04 17:46:37 +02:00
targetElement = $ ( textEditor . editing . view . domRoots . values ( ) . next ( ) . value ) . find ( findSubStr ) . filter ( function ( ) {
// When finding span[style*="color"] but not looking for span[style*="background-color"],
2023-06-03 11:55:50 +08:00
// the background-color error will be regarded as color, so it needs to be filtered
2023-06-04 17:46:37 +02:00
if ( findSubStr . indexOf ( "color" ) >= 0 && findSubStr . indexOf ( "background-color" ) < 0 ) {
2023-06-03 11:55:50 +08:00
let color = this . style . color ;
2023-06-04 17:46:37 +02:00
return ! ( $ ( this ) . prop ( 'tagName' ) === "SPAN" && color === "" ) ;
} else {
2023-06-03 11:55:50 +08:00
return true ;
2023-06-04 17:46:37 +02:00
}
} ) . filter ( function ( ) {
// Need to filter out the child elements of the element that has been found
return $ ( this ) . parent ( findSubStr ) . length === 0
&& $ ( this ) . parent ( ) . parent ( findSubStr ) . length === 0
&& $ ( this ) . parent ( ) . parent ( ) . parent ( findSubStr ) . length === 0
&& $ ( this ) . parent ( ) . parent ( ) . parent ( ) . parent ( findSubStr ) . length === 0 ;
2023-06-03 11:55:50 +08:00
} )
2023-05-31 18:32:33 +08:00
}
2023-06-04 17:46:37 +02:00
targetElement [ itemIndex ] . scrollIntoView ( {
2023-06-03 11:55:50 +08:00
behavior : "smooth" , block : "center"
} ) ;
2023-05-31 18:32:33 +08:00
}
async closeHltCommand ( ) {
2023-06-22 15:38:36 +08:00
this . noteContext . viewScope . highlightsListTemporarilyHidden = true ;
2023-05-31 18:32:33 +08:00
await this . refresh ( ) ;
this . triggerCommand ( 'reEvaluateRightPaneVisibility' ) ;
2024-09-12 19:22:41 +08:00
appContext . triggerEvent ( "reEvaluateHighlightsListWidgetVisibility" , { noteId : this . noteId } ) ;
}
async showHighlightsListWidgetEvent ( { noteId } ) {
if ( this . noteId === noteId ) {
await this . refresh ( ) ;
this . triggerCommand ( 'reEvaluateRightPaneVisibility' ) ;
}
2023-05-31 18:32:33 +08:00
}
2024-09-12 09:36:08 +08:00
async entitiesReloadedEvent ( { loadResults } ) {
2023-05-31 18:32:33 +08:00
if ( loadResults . isNoteContentReloaded ( this . noteId ) ) {
await this . refresh ( ) ;
2023-06-05 16:12:02 +02:00
} else if ( loadResults . getAttributeRows ( ) . find ( attr => attr . type === 'label'
2023-06-01 20:17:00 +08:00
&& ( attr . name . toLowerCase ( ) . includes ( 'readonly' ) || attr . name === 'hideHighlightWidget' )
2023-05-31 18:32:33 +08:00
&& attributeService . isAffecting ( attr , this . note ) ) ) {
await this . refresh ( ) ;
}
}
}