@@ -6,7 +6,7 @@ const REVIEW_OFFSETS = REVIEW_INTERVALS.reduce((offsets, interval) => {
66 return offsets ;
77} , [ ] ) ;
88const PLAN_DAY_COUNT = CHAPTER_COUNT + REVIEW_OFFSETS . at ( - 1 ) ;
9- const ANNOTATABLE_SELECTOR = "h2, h3, h4, p, li, blockquote, td, th" ;
9+ const ANNOTATABLE_SELECTOR = "h1, h2, h3, h4, h5, h6, p, li, blockquote, td, th, pre " ;
1010const DISCUSSION_NEW_URL = "https://github.com/Lling0000/Vibe_coding_guide/discussions/new?category=q-a" ;
1111const PDF_EXPORT_SCRIPTS = [
1212 "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js" ,
@@ -361,7 +361,7 @@ const copy = {
361361 annotationEmpty : "当前阅读范围还没有批注。" ,
362362 annotationNeedSelection : "先在正文里选中一段文字。" ,
363363 annotationOutside : "请选中正文里的文字。" ,
364- annotationSameBlock : "一次批注先选同一段落、标题或列表项里的文字 。" ,
364+ annotationSameBlock : "这段选择暂时无法转换成批注,请少选一点,或避开图表区域 。" ,
365365 annotationCommentPrompt : "给这段文字添加评论:" ,
366366 annotationNoComment : "没有评论" ,
367367 annotationSaved : "批注已保存。" ,
@@ -473,7 +473,7 @@ const copy = {
473473 annotationEmpty : "No annotations in the current reading range yet." ,
474474 annotationNeedSelection : "Select text in the guide first." ,
475475 annotationOutside : "Please select text inside the guide." ,
476- annotationSameBlock : "For now, keep one annotation inside the same paragraph, heading, or list item ." ,
476+ annotationSameBlock : "This selection cannot be annotated yet. Try a shorter selection or avoid diagram areas ." ,
477477 annotationCommentPrompt : "Add a comment for this text:" ,
478478 annotationNoComment : "No comment" ,
479479 annotationSaved : "Annotation saved." ,
@@ -1262,7 +1262,7 @@ function positionAnnotationPopover(range) {
12621262
12631263 els . annotationPopover . classList . toggle ( "is-below" , shouldPlaceBelow ) ;
12641264 els . annotationPopover . style . left = `${ left } px` ;
1265- els . annotationPopover . style . top = `${ Math . max ( inset , top ) } px` ;
1265+ els . annotationPopover . style . top = `${ Math . min ( window . innerHeight - inset , Math . max ( inset , top ) ) } px` ;
12661266}
12671267
12681268function updateAnnotationPopoverFromSelection ( ) {
@@ -1295,6 +1295,95 @@ function annotationRangeForAction() {
12951295 return state . annotationRange ? state . annotationRange . cloneRange ( ) : null ;
12961296}
12971297
1298+ function rangeIntersectsNode ( range , node ) {
1299+ try {
1300+ return range . intersectsNode ( node ) ;
1301+ } catch {
1302+ return false ;
1303+ }
1304+ }
1305+
1306+ function textNodeSelectionOffsets ( range , node ) {
1307+ const length = node . textContent . length ;
1308+ let start = 0 ;
1309+ let end = length ;
1310+
1311+ if ( node === range . startContainer ) {
1312+ start = range . startOffset ;
1313+ }
1314+ if ( node === range . endContainer ) {
1315+ end = range . endOffset ;
1316+ }
1317+
1318+ return {
1319+ start : Math . max ( 0 , Math . min ( length , start ) ) ,
1320+ end : Math . max ( 0 , Math . min ( length , end ) ) ,
1321+ } ;
1322+ }
1323+
1324+ function segmentsFromRange ( range ) {
1325+ const grouped = new Map ( ) ;
1326+ const walker = document . createTreeWalker ( els . article , NodeFilter . SHOW_TEXT ) ;
1327+
1328+ while ( walker . nextNode ( ) ) {
1329+ const node = walker . currentNode ;
1330+ if ( ! node . textContent || ! rangeIntersectsNode ( range , node ) ) continue ;
1331+
1332+ const block = findAnnotationBlock ( node ) ;
1333+ if ( ! block ?. dataset . blockId ) continue ;
1334+
1335+ const { start, end } = textNodeSelectionOffsets ( range , node ) ;
1336+ if ( end <= start || ! node . textContent . slice ( start , end ) . trim ( ) ) continue ;
1337+
1338+ const startOffset = offsetInBlock ( block , node , start ) ;
1339+ const endOffset = offsetInBlock ( block , node , end ) ;
1340+ const chapterIndex = Number ( block . dataset . chapterIndex ) ;
1341+ if ( startOffset < 0 || endOffset <= startOffset || ! Number . isInteger ( chapterIndex ) ) continue ;
1342+
1343+ const key = block . dataset . blockId ;
1344+ const existing = grouped . get ( key ) ;
1345+ if ( existing ) {
1346+ existing . startOffset = Math . min ( existing . startOffset , startOffset ) ;
1347+ existing . endOffset = Math . max ( existing . endOffset , endOffset ) ;
1348+ } else {
1349+ grouped . set ( key , {
1350+ block,
1351+ blockId : key ,
1352+ chapterIndex,
1353+ startOffset,
1354+ endOffset,
1355+ } ) ;
1356+ }
1357+ }
1358+
1359+ return [ ...grouped . values ( ) ]
1360+ . map ( ( segment ) => ( {
1361+ ...segment ,
1362+ quote : segment . block . textContent . slice ( segment . startOffset , segment . endOffset ) ,
1363+ } ) )
1364+ . filter ( ( segment ) => segment . quote . trim ( ) ) ;
1365+ }
1366+
1367+ function storedAnnotationSegments ( segments ) {
1368+ return segments . map ( ( { block, ...segment } ) => segment ) ;
1369+ }
1370+
1371+ function annotationSegments ( annotation ) {
1372+ if ( Array . isArray ( annotation . segments ) && annotation . segments . length ) {
1373+ return annotation . segments ;
1374+ }
1375+
1376+ return [
1377+ {
1378+ blockId : annotation . blockId ,
1379+ chapterIndex : annotation . chapterIndex ,
1380+ startOffset : annotation . startOffset ,
1381+ endOffset : annotation . endOffset ,
1382+ quote : annotation . quote ,
1383+ } ,
1384+ ] ;
1385+ }
1386+
12981387function annotationStyleLabel ( style ) {
12991388 const text = copy [ state . lang ] ;
13001389 if ( style === "underline" ) return text . annotationUnderline ;
@@ -1328,7 +1417,7 @@ function collectAnnotatableBlocks(section = null) {
13281417 } ) ;
13291418 } ) ;
13301419
1331- return blocks . filter ( ( block ) => els . article . contains ( block ) && ! block . closest ( "pre, .mermaid" ) ) ;
1420+ return blocks . filter ( ( block ) => els . article . contains ( block ) && ! block . closest ( ".mermaid" ) ) ;
13321421}
13331422
13341423function assignAnnotatableBlocks ( ) {
@@ -1343,7 +1432,7 @@ function assignAnnotatableBlocks() {
13431432function findAnnotationBlock ( node ) {
13441433 const element = node ?. nodeType === Node . TEXT_NODE ? node . parentElement : node ;
13451434 if ( ! element || ! els . article . contains ( element ) ) return null ;
1346- if ( element . closest ( "pre, .mermaid" ) ) return null ;
1435+ if ( element . closest ( ".mermaid" ) ) return null ;
13471436
13481437 const block = element . closest ( ANNOTATABLE_SELECTOR ) ;
13491438 return block && els . article . contains ( block ) ? block : null ;
@@ -1408,18 +1497,18 @@ function rangeFromOffsets(block, startOffset, endOffset) {
14081497 return null ;
14091498}
14101499
1411- function locateAnnotation ( annotation ) {
1412- const quote = annotation . quote || "" ;
1500+ function locateAnnotationSegment ( annotation , segment ) {
1501+ const quote = segment . quote || "" ;
14131502 const trimmedQuote = quote . trim ( ) ;
1414- const block = annotation . blockId
1415- ? els . article . querySelector ( `[data-block-id="${ cssEscape ( annotation . blockId ) } "]` )
1503+ const block = segment . blockId
1504+ ? els . article . querySelector ( `[data-block-id="${ cssEscape ( segment . blockId ) } "]` )
14161505 : null ;
14171506
14181507 function locateInBlock ( candidate ) {
14191508 if ( ! candidate ) return null ;
14201509 const text = candidate . textContent || "" ;
1421- const start = Number ( annotation . startOffset ) ;
1422- const end = Number ( annotation . endOffset ) ;
1510+ const start = Number ( segment . startOffset ) ;
1511+ const end = Number ( segment . endOffset ) ;
14231512
14241513 if ( Number . isFinite ( start ) && Number . isFinite ( end ) && text . slice ( start , end ) === quote ) {
14251514 return { block : candidate , startOffset : start , endOffset : end } ;
@@ -1445,7 +1534,8 @@ function locateAnnotation(annotation) {
14451534 const direct = locateInBlock ( block ) ;
14461535 if ( direct ) return direct ;
14471536
1448- const section = state . chapterSections [ annotation . chapterIndex - 1 ] || null ;
1537+ const sectionIndex = Number ( segment . chapterIndex ) || Number ( annotation . chapterIndex ) ;
1538+ const section = state . chapterSections [ sectionIndex - 1 ] || null ;
14491539 for ( const candidate of collectAnnotatableBlocks ( section ) ) {
14501540 const located = locateInBlock ( candidate ) ;
14511541 if ( located ) return located ;
@@ -1454,8 +1544,8 @@ function locateAnnotation(annotation) {
14541544 return null ;
14551545}
14561546
1457- function applyAnnotationMark ( annotation ) {
1458- const located = locateAnnotation ( annotation ) ;
1547+ function applyAnnotationMark ( annotation , segment ) {
1548+ const located = locateAnnotationSegment ( annotation , segment ) ;
14591549 if ( ! located ) return false ;
14601550
14611551 const range = rangeFromOffsets ( located . block , located . startOffset , located . endOffset ) ;
@@ -1546,12 +1636,18 @@ function renderAnnotations() {
15461636 if ( ! els . article || els . article . classList . contains ( "loading" ) ) return ;
15471637
15481638 clearAnnotationMarks ( ) ;
1549- [ ...state . annotations ]
1639+ const markSegments = state . annotations . flatMap ( ( annotation ) =>
1640+ annotationSegments ( annotation ) . map ( ( segment ) => ( { annotation, segment } ) ) ,
1641+ ) ;
1642+
1643+ markSegments
15501644 . sort ( ( a , b ) => {
1551- if ( a . blockId === b . blockId ) return Number ( b . startOffset ) - Number ( a . startOffset ) ;
1552- return String ( a . blockId ) . localeCompare ( String ( b . blockId ) ) ;
1645+ if ( a . segment . blockId === b . segment . blockId ) {
1646+ return Number ( b . segment . startOffset ) - Number ( a . segment . startOffset ) ;
1647+ }
1648+ return String ( a . segment . blockId ) . localeCompare ( String ( b . segment . blockId ) ) ;
15531649 } )
1554- . forEach ( applyAnnotationMark ) ;
1650+ . forEach ( ( { annotation , segment } ) => applyAnnotationMark ( annotation , segment ) ) ;
15551651 renderAnnotationList ( ) ;
15561652}
15571653
@@ -1568,21 +1664,13 @@ function createAnnotation(style) {
15681664 return ;
15691665 }
15701666
1571- const startBlock = findAnnotationBlock ( range . startContainer ) ;
1572- const endBlock = findAnnotationBlock ( range . endContainer ) ;
1573- if ( ! startBlock || ! endBlock || startBlock !== endBlock ) {
1667+ const segments = segmentsFromRange ( range ) ;
1668+ if ( ! segments . length ) {
15741669 setAnnotationStatus ( text . annotationSameBlock ) ;
15751670 return ;
15761671 }
15771672
1578- const startOffset = offsetInBlock ( startBlock , range . startContainer , range . startOffset ) ;
1579- const endOffset = offsetInBlock ( startBlock , range . endContainer , range . endOffset ) ;
1580- if ( startOffset < 0 || endOffset <= startOffset ) {
1581- setAnnotationStatus ( text . annotationNeedSelection ) ;
1582- return ;
1583- }
1584-
1585- const quote = startBlock . textContent . slice ( startOffset , endOffset ) ;
1673+ const quote = segments . map ( ( segment ) => segment . quote . trim ( ) ) . filter ( Boolean ) . join ( "\n\n" ) ;
15861674 if ( ! quote . trim ( ) ) {
15871675 setAnnotationStatus ( text . annotationNeedSelection ) ;
15881676 return ;
@@ -1595,7 +1683,8 @@ function createAnnotation(style) {
15951683 comment = entered . trim ( ) ;
15961684 }
15971685
1598- const chapterIndex = Number ( startBlock . dataset . chapterIndex ) ;
1686+ const firstSegment = segments [ 0 ] ;
1687+ const chapterIndex = Number ( firstSegment . chapterIndex ) ;
15991688 if ( ! Number . isInteger ( chapterIndex ) || chapterIndex < 1 ) {
16001689 setAnnotationStatus ( text . annotationOutside ) ;
16011690 return ;
@@ -1605,11 +1694,12 @@ function createAnnotation(style) {
16051694 id : annotationId ( ) ,
16061695 lang : state . lang ,
16071696 chapterIndex,
1608- blockId : startBlock . dataset . blockId ,
1697+ blockId : firstSegment . blockId ,
16091698 style,
16101699 quote,
1611- startOffset,
1612- endOffset,
1700+ startOffset : firstSegment . startOffset ,
1701+ endOffset : firstSegment . endOffset ,
1702+ segments : storedAnnotationSegments ( segments ) ,
16131703 comment,
16141704 createdAt : new Date ( ) . toISOString ( ) ,
16151705 } ;
@@ -2754,6 +2844,8 @@ els.resetMatrix.addEventListener("click", resetMatrixProgress);
27542844els . search . addEventListener ( "input" , renderToc ) ;
27552845els . top . addEventListener ( "click" , ( ) => window . scrollTo ( { top : 0 , behavior : "smooth" } ) ) ;
27562846els . annotationPopover . addEventListener ( "mousedown" , ( event ) => event . preventDefault ( ) ) ;
2847+ els . annotationPopover . addEventListener ( "pointerdown" , ( event ) => event . preventDefault ( ) ) ;
2848+ els . annotationPopover . addEventListener ( "touchstart" , ( event ) => event . preventDefault ( ) ) ;
27572849els . annotationPopover . addEventListener ( "click" , ( event ) => event . stopPropagation ( ) ) ;
27582850els . highlightButton . addEventListener ( "click" , ( ) => createAnnotation ( "highlight" ) ) ;
27592851els . underlineButton . addEventListener ( "click" , ( ) => createAnnotation ( "underline" ) ) ;
0 commit comments