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
* /
2024-09-13 11:10:59 +08:00
import { t } from "../services/i18n.js" ;
2023-05-31 18:32:33 +08:00
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" ;
2025-01-09 18:36:24 +02:00
import appContext , { type EventData } from "../components/app_context.js" ;
2024-09-12 09:36:08 +08:00
import libraryLoader from "../services/library_loader.js" ;
2025-01-13 23:18:10 +02:00
import type FNote from "../entities/fnote.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 : 10px ;
2025-01-07 12:34:10 +02:00
contain : none ;
2023-05-31 18:32:33 +08:00
overflow : auto ;
position : relative ;
}
2025-01-07 12:34:10 +02:00
2023-06-22 15:38:36 +08:00
. highlights - list > ol {
2023-05-31 18:32:33 +08:00
padding - left : 20px ;
}
2025-01-07 12:34:10 +02:00
2023-06-22 15:38:36 +08:00
. highlights - list li {
2023-05-31 18:32:33 +08:00
cursor : pointer ;
margin - bottom : 3px ;
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
}
2025-01-07 12:34:10 +02:00
2023-06-22 15:38:36 +08:00
. highlights - list li :hover {
2023-05-31 18:32:33 +08:00
font - weight : bold ;
}
< / style >
2023-06-22 15:38:36 +08:00
< span class = "highlights-list" > < / span >
2023-05-31 18:32:33 +08:00
< / div > ` ;
2023-06-04 17:46:37 +02:00
export default class HighlightsListWidget extends RightPanelWidget {
2025-01-07 12:34:10 +02:00
private $highlightsList ! : JQuery < HTMLElement > ;
2023-05-31 18:32:33 +08:00
get widgetTitle() {
2024-09-13 11:10:59 +08:00
return t ( "highlights_list_2.title" ) ;
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" )
2024-09-13 11:10:59 +08:00
. title ( t ( "highlights_list_2.options" ) )
2023-11-03 10:44:14 +01:00
. titlePlacement ( "left" )
2025-01-09 18:07:02 +02: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" )
2025-01-07 12:34:10 +02:00
. onClick ( ( widget : OnClickButtonWidget ) = > widget . triggerCommand ( "closeHlt" ) )
2023-11-03 10:44:14 +01:00
. class ( "icon-action" )
] ;
}
2023-05-31 18:32:33 +08:00
isEnabled() {
2025-01-09 18:07:02 +02:00
return (
super . isEnabled ( ) && this . note != null && this . note . type === "text" && ! this . noteContext ? . viewScope ? . highlightsListTemporarilyHidden && this . noteContext ? . viewScope ? . viewMode === "default"
) ;
2023-05-31 18:32:33 +08:00
}
async doRenderBody() {
this . $body . empty ( ) . append ( $ ( TPL ) ) ;
2025-01-09 18:07:02 +02:00
this . $highlightsList = this . $body . find ( ".highlights-list" ) ;
2023-05-31 18:32:33 +08:00
}
2025-01-07 12:34:10 +02:00
async refreshWithNote ( note : FNote | null | undefined ) {
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 * /
2025-01-07 12:34:10 +02: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
2025-01-09 18:07:02 +02:00
const optionsHighlightsList = JSON . parse ( options . get ( "highlightsList" ) ) ;
2023-06-01 20:38:55 +08:00
2025-01-09 18:07:02 +02:00
if ( note ? . isLabelTruthy ( "hideHighlightWidget" ) || ! optionsHighlightsList . length ) {
2023-05-31 18:32:33 +08:00
this . toggleInt ( false ) ;
this . triggerCommand ( "reEvaluateRightPaneVisibility" ) ;
return ;
}
2025-01-07 12:34:10 +02:00
let $highlightsList : JQuery < HTMLElement > | null = null ;
let hlLiCount = - 1 ;
2023-05-31 18:32:33 +08:00
// Check for type text unconditionally in case alwaysShowWidget is set
2025-01-09 18:07:02 +02:00
if ( note && this . note ? . type === "text" ) {
2025-01-07 12:34:10 +02:00
const noteComplement = await note . getNoteComplement ( ) ;
if ( noteComplement && "content" in noteComplement ) {
( { $highlightsList , hlLiCount } = await this . getHighlightList ( noteComplement . content , optionsHighlightsList ) ) ;
}
}
this . $highlightsList . empty ( ) ;
if ( $highlightsList ) {
this . $highlightsList . append ( $highlightsList ) ;
2023-05-31 18:32:33 +08:00
}
2023-06-22 15:38:36 +08:00
if ( hlLiCount > 0 ) {
2023-06-01 20:17:00 +08:00
this . toggleInt ( true ) ;
2025-01-07 12:34:10 +02:00
if ( this . noteContext ? . viewScope ) {
this . noteContext . viewScope . highlightsListPreviousVisible = true ;
}
2023-06-01 20:17:00 +08:00
} else {
this . toggleInt ( false ) ;
2025-01-07 12:34:10 +02:00
if ( this . noteContext ? . viewScope ) {
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
2025-01-07 12:34:10 +02:00
extractOuterTag ( htmlStr : string | null ) {
2024-09-12 09:36:08 +08:00
if ( htmlStr === null ) {
2025-01-09 18:07:02 +02:00
return null ;
2024-09-12 09:36:08 +08:00
}
// 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 ;
}
2025-01-07 12:34:10 +02:00
areOuterTagsConsistent ( str1 : string | null , str2 : string | null ) {
2024-09-12 09:36:08 +08:00
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
*
2025-01-07 12:34:10 +02:00
* @param html Note ' s html content
* @returns The HTML content with mathematical formulas rendered by KaTeX .
2024-09-12 09:36:08 +08:00
* /
2025-01-07 12:34:10 +02:00
async replaceMathTextWithKatax ( html : string ) {
2024-09-12 09:36:08 +08:00
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 ) {
2025-01-09 18:07:02 +02:00
if ( e instanceof ReferenceError && e . message . includes ( "katex is not defined" ) ) {
2024-09-12 09:36:08 +08:00
// 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 ;
}
2025-01-07 12:34:10 +02:00
async getHighlightList ( content : string , optionsHighlightsList : string [ ] ) {
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
2025-03-29 14:08:57 +02:00
const regex3 = /(<i>[\s\S]*?<\/i>|<em>[\s\S]*?<\/em>)/gi ;
2023-06-03 11:55:50 +08:00
// 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
2025-01-09 18:07:02 +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
}
2025-01-09 18:07:02 +02:00
findSubStr = findSubStr . substring ( 1 ) ;
2023-06-03 11:55:50 +08:00
combinedRegexStr = ` ( ` + combinedRegexStr . substring ( 1 ) + ` ) ` ;
2025-01-09 18:07:02 +02:00
const combinedRegex = new RegExp ( combinedRegexStr , "gi" ) ;
2023-06-04 17:46:37 +02:00
const $highlightsList = $ ( "<ol>" ) ;
2025-01-09 18:07:02 +02:00
let prevEndIndex = - 1 ,
hlLiCount = 0 ;
2025-01-07 12:34:10 +02:00
let prevSubHtml : string | null = null ;
2024-09-12 09:36:08 +08:00
// Used to determine if a string is only a formula
const onlyMathRegex = /^<span class="math-tex">\\\([^\)]*?\)<\/span>(?:<span class="math-tex">\\\([^\)]*?\)<\/span>)*$/ ;
2025-01-09 18:07:02 +02:00
for ( let match = null , hltIndex = 0 ; ( match = combinedRegex . exec ( content ) ) !== null ; hltIndex ++ ) {
2023-06-04 17:46:37 +02:00
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 ;
2025-01-07 12:38:50 +02:00
// Ignore footnotes.
if ( subHtml . startsWith ( '<strong><a href="#fnref' ) ) {
continue ;
}
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 ) ) {
2025-01-09 18:07:02 +02:00
const $lastLi = $highlightsList . children ( "li" ) . last ( ) ;
2024-09-12 09:36:08 +08:00
$lastLi . append ( await this . replaceMathTextWithKatax ( substring ) ) ;
$lastLi . append ( subHtml ) ;
} else {
$highlightsList . append (
2025-01-09 18:07:02 +02:00
$ ( "<li>" )
2024-09-12 09:36:08 +08:00
. 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
2025-01-07 12:34:10 +02:00
async jumpToHighlightsList ( findSubStr : string , itemIndex : number ) {
if ( ! this . noteContext ) {
return ;
}
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 ( ) ;
2025-01-09 18:07:02 +02:00
targetElement = $container
. find ( findSubStr )
. filter ( function ( ) {
2025-01-07 12:34:10 +02:00
if ( findSubStr . indexOf ( "color" ) >= 0 && findSubStr . indexOf ( "background-color" ) < 0 ) {
let color = this . style . color ;
2025-01-09 18:07:02 +02:00
const $el = $ ( this as HTMLElement ) ;
return ! ( $el . prop ( "tagName" ) === "SPAN" && color === "" ) ;
2025-01-07 12:34:10 +02:00
} else {
return true ;
}
} )
2025-01-09 18:07:02 +02:00
. filter ( function ( ) {
const $el = $ ( this as HTMLElement ) ;
return (
$el . parent ( findSubStr ) . length === 0 &&
$el . parent ( ) . parent ( findSubStr ) . length === 0 &&
$el . parent ( ) . parent ( ) . parent ( findSubStr ) . length === 0 &&
$el . parent ( ) . parent ( ) . parent ( ) . parent ( findSubStr ) . length === 0
) ;
} ) ;
} else {
const textEditor = await this . noteContext . getTextEditor ( ) ;
if ( textEditor ) {
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"],
// the background-color error will be regarded as color, so it needs to be filtered
const $el = $ ( this as HTMLElement ) ;
if ( findSubStr . indexOf ( "color" ) >= 0 && findSubStr . indexOf ( "background-color" ) < 0 ) {
let color = this . style . color ;
return ! ( $el . prop ( "tagName" ) === "SPAN" && color === "" ) ;
} else {
return true ;
}
} )
. filter ( function ( ) {
// Need to filter out the child elements of the element that has been found
const $el = $ ( this as HTMLElement ) ;
return (
$el . parent ( findSubStr ) . length === 0 &&
$el . parent ( ) . parent ( findSubStr ) . length === 0 &&
$el . parent ( ) . parent ( ) . parent ( findSubStr ) . length === 0 &&
$el . parent ( ) . parent ( ) . parent ( ) . parent ( findSubStr ) . length === 0
) ;
} ) ;
2025-01-07 12:34:10 +02:00
}
}
2025-03-29 14:10:12 +02:00
if ( targetElement && targetElement [ itemIndex ] ) {
2025-01-07 12:34:10 +02:00
targetElement [ itemIndex ] . scrollIntoView ( {
2025-01-09 18:07:02 +02:00
behavior : "smooth" ,
block : "center"
2025-01-07 12:34:10 +02:00
} ) ;
2025-03-29 14:10:12 +02:00
} else {
console . warn ( "Unable to find the target element in the highlights list." ) ;
2023-05-31 18:32:33 +08:00
}
}
async closeHltCommand() {
2025-01-07 12:34:10 +02:00
if ( this . noteContext ? . viewScope ) {
this . noteContext . viewScope . highlightsListTemporarilyHidden = true ;
}
2023-05-31 18:32:33 +08:00
await this . refresh ( ) ;
2025-01-09 18:07:02 +02:00
this . triggerCommand ( "reEvaluateRightPaneVisibility" ) ;
2024-09-12 19:22:41 +08:00
appContext . triggerEvent ( "reEvaluateHighlightsListWidgetVisibility" , { noteId : this.noteId } ) ;
}
2025-01-07 12:34:10 +02:00
async showHighlightsListWidgetEvent ( { noteId } : EventData < "showHighlightsListWidget" > ) {
2024-09-12 19:22:41 +08:00
if ( this . noteId === noteId ) {
await this . refresh ( ) ;
2025-01-09 18:07:02 +02:00
this . triggerCommand ( "reEvaluateRightPaneVisibility" ) ;
2024-09-12 19:22:41 +08:00
}
2023-05-31 18:32:33 +08:00
}
2025-01-07 12:34:10 +02:00
async entitiesReloadedEvent ( { loadResults } : EventData < "entitiesReloaded" > ) {
if ( this . noteId && loadResults . isNoteContentReloaded ( this . noteId ) ) {
2023-05-31 18:32:33 +08:00
await this . refresh ( ) ;
2025-01-09 18:07:02 +02:00
} else if (
loadResults
. getAttributeRows ( )
. find ( ( attr ) = > attr . type === "label" && ( attr . name ? . toLowerCase ( ) . includes ( "readonly" ) || attr . name === "hideHighlightWidget" ) && attributeService . isAffecting ( attr , this . note ) )
) {
2023-05-31 18:32:33 +08:00
await this . refresh ( ) ;
}
}
}