11import { processOutputMarks } from '@converter/exporter.js' ;
22import { TrackFormatMarkName } from '@extensions/track-changes/constants.js' ;
33
4+ export const ParagraphSplitSnapshotType = 'paragraphSplit' ;
5+
46const getMarkType = ( mark ) => mark ?. type ?. name ?? mark ?. type ?? null ;
7+ const getSnapshotType = ( snapshot ) => snapshot ?. type ?. name ?? snapshot ?. type ?? null ;
8+ const isDecimalString = ( value ) => typeof value === 'string' && / ^ \d + $ / . test ( value ) ;
59
610const toRunPropertyElements = ( marks = [ ] ) =>
711 processOutputMarks ( marks ) . filter ( ( element ) => element && typeof element === 'object' && element . name ) ;
812
13+ const hashStringToDecimalId = ( value ) => {
14+ const source = String ( value || '0' ) ;
15+ let hash = 0 ;
16+ for ( let i = 0 ; i < source . length ; i ++ ) {
17+ hash = ( hash * 31 + source . charCodeAt ( i ) ) >>> 0 ;
18+ }
19+ return String ( hash || 1 ) ;
20+ } ;
21+
22+ const getTrackFormatChangeWordId = ( trackFormatMark , options = { } ) => {
23+ const allocator = options ?. wordIdAllocator || null ;
24+ const partPath = options ?. partPath || 'word/document.xml' ;
25+ const sourceId = trackFormatMark . attrs ?. sourceId == null ? '' : String ( trackFormatMark . attrs . sourceId ) ;
26+ const logicalId = trackFormatMark . attrs ?. id == null ? '' : String ( trackFormatMark . attrs . id ) ;
27+
28+ if ( allocator ) return allocator . allocate ( { partPath, sourceId, logicalId } ) ;
29+ if ( isDecimalString ( sourceId ) ) return sourceId ;
30+ if ( isDecimalString ( logicalId ) ) return logicalId ;
31+ return hashStringToDecimalId ( sourceId || logicalId ) ;
32+ } ;
33+
34+ const getTrackChangeAuthor = ( trackFormatMark ) => {
35+ const author = trackFormatMark . attrs ?. author ;
36+ return author == null ? '' : String ( author ) ;
37+ } ;
38+
939/**
1040 * Return the first trackFormat mark from a mark list.
1141 *
@@ -15,6 +45,35 @@ const toRunPropertyElements = (marks = []) =>
1545export const findTrackFormatMark = ( marks = [ ] ) =>
1646 marks . find ( ( mark ) => getMarkType ( mark ) === TrackFormatMarkName ) ?? null ;
1747
48+ export const findSnapshotByType = ( snapshots = [ ] , type ) =>
49+ Array . isArray ( snapshots ) ? ( snapshots . find ( ( snapshot ) => getSnapshotType ( snapshot ) === type ) ?? null ) : null ;
50+
51+ export const findParagraphSplitSnapshot = ( trackFormatMark ) => {
52+ if ( ! trackFormatMark ) return null ;
53+ return (
54+ findSnapshotByType ( trackFormatMark . attrs ?. before , ParagraphSplitSnapshotType ) ||
55+ findSnapshotByType ( trackFormatMark . attrs ?. after , ParagraphSplitSnapshotType )
56+ ) ;
57+ } ;
58+
59+ export const isParagraphSplitTrackFormatMark = ( mark ) =>
60+ getMarkType ( mark ) === TrackFormatMarkName && Boolean ( findParagraphSplitSnapshot ( mark ) ) ;
61+
62+ export const createParagraphSplitInsertionElement = ( trackFormatMark , options = { } ) => {
63+ const paragraphSplit = findParagraphSplitSnapshot ( trackFormatMark ) ;
64+ if ( ! paragraphSplit ) return undefined ;
65+
66+ return {
67+ type : 'element' ,
68+ name : 'w:ins' ,
69+ attributes : {
70+ 'w:id' : getTrackFormatChangeWordId ( trackFormatMark , options ) ,
71+ 'w:author' : getTrackChangeAuthor ( trackFormatMark ) ,
72+ 'w:date' : trackFormatMark . attrs ?. date ,
73+ } ,
74+ } ;
75+ } ;
76+
1877/**
1978 * Build a valid OOXML <w:rPrChange> node from a trackFormat mark.
2079 *
@@ -35,22 +94,21 @@ export const createRunPropertiesChangeElement = (trackFormatMark, options = {})
3594 elements : toRunPropertyElements ( beforeMarks ) ,
3695 } ;
3796
38- // Phase 005 — if an allocator was passed in, mint a Word-native decimal
39- // `w:id`. Legacy callers (no `options.wordIdAllocator`) keep the prior
40- // `sourceId || id` behavior so the exported byte stream is unchanged.
41- const allocator = options ?. wordIdAllocator || null ;
42- const partPath = options ?. partPath || 'word/document.xml' ;
43- const sourceId = trackFormatMark . attrs ?. sourceId ;
44- const logicalId = trackFormatMark . attrs ?. id ;
45- const wordId = allocator ? allocator . allocate ( { partPath, sourceId, logicalId } ) : sourceId || logicalId ;
97+ // Prefer the export allocator for Word-native revision ids. Legacy callers
98+ // without an allocator still need decimal OOXML ids, so they use a decimal
99+ // source/logical id when available and a deterministic decimal fallback
100+ // otherwise.
101+ const wordId = getTrackFormatChangeWordId ( trackFormatMark , options ) ;
46102
103+ // w:authorEmail is not part of the OOXML CT_TrackChange attribute set, so it is
104+ // intentionally omitted from <w:rPrChange>. The author email remains available on
105+ // the internal trackFormat mark attrs for editor-side use; it is just not serialized.
47106 return {
48107 type : 'element' ,
49108 name : 'w:rPrChange' ,
50109 attributes : {
51110 'w:id' : wordId ,
52- 'w:author' : trackFormatMark . attrs ?. author ,
53- 'w:authorEmail' : trackFormatMark . attrs ?. authorEmail ,
111+ 'w:author' : getTrackChangeAuthor ( trackFormatMark ) ,
54112 'w:date' : trackFormatMark . attrs ?. date ,
55113 } ,
56114 elements : [ previousRunProperties ] ,
0 commit comments