Monorepo for Tangled tangled.org

appview/pages: initial support for split diffs

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 4c477c06 56590c67

verified
Changed files
+170 -46
appview
types
+3
appview/pages/pages.go
··· 527 Active string 528 EmailToDidOrHandle map[string]string 529 Pipeline *db.Pipeline 530 531 // singular because it's always going to be just one 532 VerifiedCommit commitverify.VerifiedCommits ··· 859 Round int 860 Submission *db.PullSubmission 861 OrderedReactionKinds []db.ReactionKind 862 } 863 864 // this name is a mouthful ··· 964 Base string 965 Head string 966 Diff *types.NiceDiff 967 968 Active string 969 }
··· 527 Active string 528 EmailToDidOrHandle map[string]string 529 Pipeline *db.Pipeline 530 + DiffOpts types.DiffOpts 531 532 // singular because it's always going to be just one 533 VerifiedCommit commitverify.VerifiedCommits ··· 860 Round int 861 Submission *db.PullSubmission 862 OrderedReactionKinds []db.ReactionKind 863 + DiffOpts types.DiffOpts 864 } 865 866 // this name is a mouthful ··· 966 Base string 967 Head string 968 Diff *types.NiceDiff 969 + DiffOpts types.DiffOpts 970 971 Active string 972 }
+1 -1
appview/pages/templates/repo/commit.html
··· 119 {{ end }} 120 121 {{ define "contentAfter" }} 122 - {{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff) }} 123 {{end}} 124 125 {{ define "contentAfterLeft" }}
··· 119 {{ end }} 120 121 {{ define "contentAfter" }} 122 + {{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff .DiffOpts) }} 123 {{end}} 124 125 {{ define "contentAfterLeft" }}
+1 -1
appview/pages/templates/repo/compare/compare.html
··· 50 {{ end }} 51 52 {{ define "contentAfter" }} 53 - {{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff) }} 54 {{end}} 55 56 {{ define "contentAfterLeft" }}
··· 50 {{ end }} 51 52 {{ define "contentAfter" }} 53 + {{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff .DiffOpts) }} 54 {{end}} 55 56 {{ define "contentAfterLeft" }}
+118 -39
appview/pages/templates/repo/fragments/diff.html
··· 1 {{ define "repo/fragments/diff" }} 2 {{ $repo := index . 0 }} 3 {{ $diff := index . 1 }} 4 {{ $commit := $diff.Commit }} 5 {{ $diff := $diff.Diff }} 6 {{ $this := $commit.This }} 7 {{ $parent := $commit.Parent }} 8 ··· 85 This is a binary file and will not be displayed. 86 </p> 87 {{ else }} 88 - {{ $name := .Name.New }} 89 - <pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 90 - {{- $oldStart := .OldPosition -}} 91 - {{- $newStart := .NewPosition -}} 92 - {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 scroll-mt-10 target:border target:border-amber-500 target:rounded " -}} 93 - {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 94 - {{- $lineNrSepStyle1 := "" -}} 95 - {{- $lineNrSepStyle2 := "pr-2" -}} 96 - {{- range .Lines -}} 97 - {{- if eq .Op.String "+" -}} 98 - <div class="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 flex min-w-full items-center"> 99 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></div> 100 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}" id="{{$name}}-N{{$newStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div> 101 - <div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div> 102 - <div class="px-2">{{ .Line }}</div> 103 - </div> 104 - {{- $newStart = add64 $newStart 1 -}} 105 - {{- end -}} 106 - {{- if eq .Op.String "-" -}} 107 - <div class="bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 flex min-w-full items-center"> 108 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}" id="{{$name}}-O{{$oldStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div> 109 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></div> 110 - <div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div> 111 - <div class="px-2">{{ .Line }}</div> 112 - </div> 113 - {{- $oldStart = add64 $oldStart 1 -}} 114 - {{- end -}} 115 - {{- if eq .Op.String " " -}} 116 - <div class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 flex min-w-full items-center"> 117 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}" id="{{$name}}-O{{$oldStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div> 118 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}" id="{{$name}}-N{{$newStart}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div> 119 - <div class="w-5 flex-shrink-0 select-none text-center">{{ .Op.String }}</div> 120 - <div class="px-2">{{ .Line }}</div> 121 - </div> 122 - {{- $newStart = add64 $newStart 1 -}} 123 - {{- $oldStart = add64 $oldStart 1 -}} 124 - {{- end -}} 125 - {{- end -}} 126 - {{- end -}}</div></div></pre> 127 {{- end -}} 128 </div> 129 ··· 136 {{ end }} 137 {{ end }} 138 139 {{ define "statPill" }} 140 <div class="flex items-center font-mono text-sm"> 141 {{ if and .Insertions .Deletions }} ··· 148 {{ end }} 149 </div> 150 {{ end }}
··· 1 {{ define "repo/fragments/diff" }} 2 {{ $repo := index . 0 }} 3 {{ $diff := index . 1 }} 4 + {{ $opts := index . 2 }} 5 + 6 {{ $commit := $diff.Commit }} 7 {{ $diff := $diff.Diff }} 8 + {{ $isSplit := $opts.Split }} 9 {{ $this := $commit.This }} 10 {{ $parent := $commit.Parent }} 11 ··· 88 This is a binary file and will not be displayed. 89 </p> 90 {{ else }} 91 + {{ if $isSplit }} 92 + {{- template "repo/fragments/splitDiff" .Split -}} 93 + {{ else }} 94 + {{- template "repo/fragments/unifiedDiff" . -}} 95 + {{ end }} 96 {{- end -}} 97 </div> 98 ··· 105 {{ end }} 106 {{ end }} 107 108 + {{ define "unifiedDiffLines" }} 109 + {{ $name := .Name.New }} 110 + <pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 111 + {{- $oldStart := .OldPosition -}} 112 + {{- $newStart := .NewPosition -}} 113 + {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 target:bg-yellow-200 target:dark:bg-yellow-600" -}} 114 + {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 115 + {{- $lineNrSepStyle1 := "" -}} 116 + {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 117 + {{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 118 + {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 119 + {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 120 + {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} 121 + {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} 122 + {{- range .Lines -}} 123 + {{- if eq .Op.String "+" -}} 124 + <div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}"> 125 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></div> 126 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div> 127 + <div class="{{ $opStyle }}">{{ .Op.String }}</div> 128 + <div class="px-2">{{ .Line }}</div> 129 + </div> 130 + {{- $newStart = add64 $newStart 1 -}} 131 + {{- end -}} 132 + {{- if eq .Op.String "-" -}} 133 + <div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}"> 134 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div> 135 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></div> 136 + <div class="{{ $opStyle }}">{{ .Op.String }}</div> 137 + <div class="px-2">{{ .Line }}</div> 138 + </div> 139 + {{- $oldStart = add64 $oldStart 1 -}} 140 + {{- end -}} 141 + {{- if eq .Op.String " " -}} 142 + <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}"> 143 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></div> 144 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></div> 145 + <div class="{{ $opStyle }}">{{ .Op.String }}</div> 146 + <div class="px-2">{{ .Line }}</div> 147 + </div> 148 + {{- $newStart = add64 $newStart 1 -}} 149 + {{- $oldStart = add64 $oldStart 1 -}} 150 + {{- end -}} 151 + {{- end -}} 152 + {{- end -}}</div></div></pre> 153 + {{ end }} 154 + 155 + {{ define "splitDiffLines" }} 156 + {{ $name := .Name.New }} 157 + {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} 158 + {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 159 + {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 160 + {{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 161 + {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 162 + {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 163 + {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 164 + {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} 165 + {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} 166 + <div class="grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-700"> 167 + <pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 168 + {{- range .LeftLines -}} 169 + {{- if .IsEmpty -}} 170 + <div class="{{ $emptyStyle }} {{ $containerStyle }}"> 171 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div> 172 + <div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div> 173 + <div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div> 174 + </div> 175 + {{- else if eq .Op.String "-" -}} 176 + <div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 177 + <div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div> 178 + <div class="{{ $opStyle }}">{{ .Op.String }}</div> 179 + <div class="px-2">{{ .Content }}</div> 180 + </div> 181 + {{- else if eq .Op.String " " -}} 182 + <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 183 + <div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div> 184 + <div class="{{ $opStyle }}">{{ .Op.String }}</div> 185 + <div class="px-2">{{ .Content }}</div> 186 + </div> 187 + {{- end -}} 188 + {{- end -}} 189 + {{- end -}}</div></div></pre> 190 + 191 + <pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 192 + {{- range .RightLines -}} 193 + {{- if .IsEmpty -}} 194 + <div class="{{ $emptyStyle }} {{ $containerStyle }}"> 195 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div> 196 + <div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div> 197 + <div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div> 198 + </div> 199 + {{- else if eq .Op.String "+" -}} 200 + <div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 201 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div> 202 + <div class="{{ $opStyle }}">{{ .Op.String }}</div> 203 + <div class="px-2" >{{ .Content }}</div> 204 + </div> 205 + {{- else if eq .Op.String " " -}} 206 + <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 207 + <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div> 208 + <div class="{{ $opStyle }}">{{ .Op.String }}</div> 209 + <div class="px-2">{{ .Content }}</div> 210 + </div> 211 + {{- end -}} 212 + {{- end -}} 213 + {{- end -}}</div></div></pre> 214 + </div> 215 + {{ end }} 216 + 217 {{ define "statPill" }} 218 <div class="flex items-center font-mono text-sm"> 219 {{ if and .Insertions .Deletions }} ··· 226 {{ end }} 227 </div> 228 {{ end }} 229 +
+1 -1
appview/pages/templates/repo/fragments/diffChangedFiles.html
··· 2 {{ $stat := .Stat }} 3 {{ $fileTree := fileTree .ChangedFiles }} 4 <div class="col-span-1 md:col-span-2 mt-4"> 5 - <section class="sticky top-0 overflow-x-auto px-6 py-4 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 6 <div class="diff-stat"> 7 <div class="flex gap-2 items-center"> 8 <strong class="text-sm uppercase dark:text-gray-200">Changed files</strong>
··· 2 {{ $stat := .Stat }} 3 {{ $fileTree := fileTree .ChangedFiles }} 4 <div class="col-span-1 md:col-span-2 mt-4"> 5 + <section class="sticky top-0 overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto md:min-h-screen rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 6 <div class="diff-stat"> 7 <div class="flex gap-2 items-center"> 8 <strong class="text-sm uppercase dark:text-gray-200">Changed files</strong>
+1 -1
appview/pages/templates/repo/fragments/interdiffFiles.html
··· 1 {{ define "repo/fragments/interdiffFiles" }} 2 {{ $fileTree := fileTree .AffectedFiles }} 3 - <section class="mt-4 p-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 4 <div class="diff-stat"> 5 <div class="flex gap-2 items-center"> 6 <strong class="text-sm uppercase dark:text-gray-200">files</strong>
··· 1 {{ define "repo/fragments/interdiffFiles" }} 2 {{ $fileTree := fileTree .AffectedFiles }} 3 + <section class="mt-4 px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm md:min-h-screen text-sm"> 4 <div class="diff-stat"> 5 <div class="flex gap-2 items-center"> 6 <strong class="text-sm uppercase dark:text-gray-200">files</strong>
+1 -1
appview/pages/templates/repo/pulls/patch.html
··· 74 {{ end }} 75 76 {{ define "contentAfter" }} 77 - {{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff) }} 78 {{end}} 79 80 {{ define "contentAfterLeft" }}
··· 74 {{ end }} 75 76 {{ define "contentAfter" }} 77 + {{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff .DiffOpts) }} 78 {{end}} 79 80 {{ define "contentAfterLeft" }}
-1
appview/pages/templates/repo/pulls/pull.html
··· 179 {{ end }} 180 </div> 181 </details> 182 - <hr class="md:hidden border-t border-gray-300 dark:border-gray-600"/> 183 {{ end }} 184 {{ end }} 185 {{ end }}
··· 179 {{ end }} 180 </div> 181 </details> 182 {{ end }} 183 {{ end }} 184 {{ end }}
+6
appview/pulls/pulls.go
··· 355 return 356 } 357 358 pull, ok := r.Context().Value("pull").(*db.Pull) 359 if !ok { 360 log.Println("failed to get pull") ··· 395 Round: roundIdInt, 396 Submission: pull.Submissions[roundIdInt], 397 Diff: &diff, 398 }) 399 400 }
··· 355 return 356 } 357 358 + var diffOpts types.DiffOpts 359 + if d := r.URL.Query().Get("diff"); d == "split" { 360 + diffOpts.Split = true 361 + } 362 + 363 pull, ok := r.Context().Value("pull").(*db.Pull) 364 if !ok { 365 log.Println("failed to get pull") ··· 400 Round: roundIdInt, 401 Submission: pull.Submissions[roundIdInt], 402 Diff: &diff, 403 + DiffOpts: diffOpts, 404 }) 405 406 }
+12 -1
appview/repo/repo.go
··· 268 protocol = "https" 269 } 270 271 if !plumbing.IsHash(ref) { 272 rp.pages.Error404(w) 273 return ··· 321 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap), 322 VerifiedCommit: vc, 323 Pipeline: pipeline, 324 }) 325 - return 326 } 327 328 func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) { ··· 1269 return 1270 } 1271 1272 // if user is navigating to one of 1273 // /compare/{base}/{head} 1274 // /compare/{base}...{head} ··· 1331 Base: base, 1332 Head: head, 1333 Diff: &diff, 1334 }) 1335 1336 }
··· 268 protocol = "https" 269 } 270 271 + var diffOpts types.DiffOpts 272 + if d := r.URL.Query().Get("diff"); d == "split" { 273 + diffOpts.Split = true 274 + } 275 + 276 if !plumbing.IsHash(ref) { 277 rp.pages.Error404(w) 278 return ··· 326 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap), 327 VerifiedCommit: vc, 328 Pipeline: pipeline, 329 + DiffOpts: diffOpts, 330 }) 331 } 332 333 func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) { ··· 1274 return 1275 } 1276 1277 + var diffOpts types.DiffOpts 1278 + if d := r.URL.Query().Get("diff"); d == "split" { 1279 + diffOpts.Split = true 1280 + } 1281 + 1282 // if user is navigating to one of 1283 // /compare/{base}/{head} 1284 // /compare/{base}...{head} ··· 1341 Base: base, 1342 Head: head, 1343 Diff: &diff, 1344 + DiffOpts: diffOpts, 1345 }) 1346 1347 }
+26
types/diff.go
··· 5 "github.com/go-git/go-git/v5/plumbing/object" 6 ) 7 8 type TextFragment struct { 9 Header string `json:"comment"` 10 Lines []gitdiff.Line `json:"lines"` ··· 77 78 return files 79 }
··· 5 "github.com/go-git/go-git/v5/plumbing/object" 6 ) 7 8 + type DiffOpts struct { 9 + Split bool `json:"split"` 10 + } 11 + 12 type TextFragment struct { 13 Header string `json:"comment"` 14 Lines []gitdiff.Line `json:"lines"` ··· 81 82 return files 83 } 84 + 85 + // used by html elements as a unique ID for hrefs 86 + func (d *Diff) Id() string { 87 + return d.Name.New 88 + } 89 + 90 + func (d *Diff) Split() *SplitDiff { 91 + fragments := make([]SplitFragment, len(d.TextFragments)) 92 + for i, fragment := range d.TextFragments { 93 + leftLines, rightLines := SeparateLines(&fragment) 94 + fragments[i] = SplitFragment{ 95 + Header: fragment.Header(), 96 + LeftLines: leftLines, 97 + RightLines: rightLines, 98 + } 99 + } 100 + 101 + return &SplitDiff{ 102 + Name: d.Id(), 103 + TextFragments: fragments, 104 + } 105 + }