Skip to content

Commit c0fd150

Browse files
committed
Add folding support to code blocks
1 parent 803b526 commit c0fd150

File tree

5 files changed

+250
-58
lines changed

5 files changed

+250
-58
lines changed

site/lib/_sass/components/_code.scss

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,111 @@ pre {
142142
span.line {
143143
padding-left: 0.5rem;
144144

145-
&[data-line]::before {
146-
display: inline-block;
147-
content: attr(data-line) "";
148-
width: 2em;
149-
margin-right: 0.5rem;
150-
text-align: right;
151-
color: var(--site-base-fgColor-alt);
145+
&[data-line] {
146+
padding-left: 0;
147+
148+
&::before {
149+
display: inline-block;
150+
content: attr(data-line) "";
151+
width: 2em;
152+
margin-right: 1rem;
153+
text-align: right;
154+
color: var(--site-base-fgColor-alt);
155+
}
156+
}
157+
}
158+
}
159+
160+
&.show-folding-ranges code {
161+
162+
--folding-inset: 0px;
163+
164+
// A folding range.
165+
details {
166+
margin: 0;
167+
position: relative;
168+
169+
--level: 0;
170+
--inset: calc(var(--folding-inset) + 2px + (var(--level) * 1ch));
171+
172+
// Vertical line drawn along a folding range.
173+
&::before {
174+
content: "";
175+
display: none;
176+
position: absolute;
177+
// inset + caret width/2 - line width/2
178+
left: calc(var(--inset) + .5ch - .5px);
179+
width: 1px;
180+
background: var(--site-inset-borderColor);
181+
height: calc(100% - .8lh);
182+
top: .8lh;
183+
}
184+
185+
summary {
186+
margin: 0;
187+
font-weight: initial;
188+
user-select: initial;
189+
list-style: none;
190+
position: relative;
191+
192+
// Caret at the beginning of the folding range.
193+
&::before {
194+
content: "^";
195+
position: absolute;
196+
cursor: pointer;
197+
z-index: var(--site-z-base);
198+
left: var(--inset);
199+
top: 50%;
200+
height: 1.3em;
201+
translate: 0 -50%;
202+
transition: transform 300ms ease;
203+
transform: rotate(0);
204+
transform-origin: center;
205+
}
206+
207+
&:hover>span {
208+
color: initial;
209+
}
210+
}
211+
212+
&:not([open])>summary .line {
213+
>span {
214+
opacity: 0.5;
215+
}
216+
217+
>span::after {
218+
content: '';
219+
}
220+
}
221+
222+
&[open] {
223+
&::before {
224+
display: block;
225+
}
226+
227+
>summary {
228+
cursor: auto;
229+
230+
&::before {
231+
transform: rotate(180deg);
232+
}
233+
}
234+
}
235+
236+
>* {
237+
margin: 0;
238+
}
239+
}
240+
}
241+
242+
&.show-line-numbers.show-folding-ranges code {
243+
--folding-inset: 2.5em;
244+
245+
span.line {
246+
&[data-line] {
247+
&::before {
248+
margin-right: 3rem;
249+
}
152250
}
153251
}
154252
}

site/lib/src/components/common/wrapped_code_block.dart

Lines changed: 62 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ final class WrappedCodeBlock extends StatelessComponent {
1919
this.highlightLines = const {},
2020
this.addedLines = const {},
2121
this.removedLines = const {},
22+
this.foldingRanges = const [],
2223
this.languagesToHide = const {'plaintext', 'console'},
2324
this.tag,
2425
this.initialLineNumber = 1,
@@ -35,6 +36,7 @@ final class WrappedCodeBlock extends StatelessComponent {
3536
final Set<int> highlightLines;
3637
final Set<int> addedLines;
3738
final Set<int> removedLines;
39+
final List<FoldingRange> foldingRanges;
3840
final Set<String> languagesToHide;
3941
final CodeBlockTag? tag;
4042
final int initialLineNumber;
@@ -43,14 +45,64 @@ final class WrappedCodeBlock extends StatelessComponent {
4345

4446
@override
4547
Component build(BuildContext context) {
48+
final children = <int, Component>{
49+
for (var lineIndex = 0; lineIndex < content.length; lineIndex += 1)
50+
lineIndex: span(
51+
classes: [
52+
'line',
53+
if (highlightLines.contains(lineIndex + 1)) 'highlighted-line',
54+
if (removedLines.contains(lineIndex + 1)) 'removed-line',
55+
if (addedLines.contains(lineIndex + 1)) 'added-line',
56+
].toClasses,
57+
attributes: {
58+
if (showLineNumbers)
59+
'data-line': '${initialLineNumber + lineIndex}',
60+
},
61+
[
62+
switch (content[lineIndex]) {
63+
// Add a zero-width space when empty
64+
// so that the line isn't collapsed to 0 height.
65+
final line when line.isEmpty => span(
66+
styles: const Styles(
67+
userSelect: UserSelect.none,
68+
),
69+
[text('\u200b')],
70+
),
71+
final lineSpans => span(lineSpans),
72+
},
73+
text('\n'),
74+
],
75+
),
76+
};
77+
78+
if (foldingRanges.isNotEmpty) {
79+
for (final (:start, :end, :level, :open) in foldingRanges) {
80+
final foldingSummary = children.remove(start - 1);
81+
final foldedChildren = [
82+
for (var i = start+1; i <= end; i += 1) ?children.remove(i-1),
83+
];
84+
85+
children[start - 1] = details(
86+
open: open,
87+
styles: Styles(raw: {'--level': '$level'}),
88+
[
89+
summary(
90+
classes: 'fold-summary',
91+
[foldingSummary ?? text('...')],
92+
),
93+
...foldedChildren,
94+
],
95+
);
96+
}
97+
}
98+
4699
return div(
47100
classes: 'code-block-wrapper language-$language',
48101
[
49102
if (title case final title?)
50-
div(
51-
classes: 'code-block-header',
52-
[text(title)],
53-
),
103+
div(classes: 'code-block-header', [
104+
text(title),
105+
]),
54106
div(
55107
classes: [
56108
'code-block-body',
@@ -71,47 +123,14 @@ final class WrappedCodeBlock extends StatelessComponent {
71123
pre(
72124
classes: [
73125
if (showLineNumbers) 'show-line-numbers',
126+
if (foldingRanges.isNotEmpty) 'show-folding-ranges',
74127
'opal',
75128
].toClasses,
76129
attributes: {'tabindex': '0'},
77130
[
78-
code(
79-
[
80-
for (
81-
var lineIndex = 0;
82-
lineIndex < content.length;
83-
lineIndex += 1
84-
)
85-
span(
86-
classes: [
87-
'line',
88-
if (highlightLines.contains(lineIndex + 1))
89-
'highlighted-line',
90-
if (removedLines.contains(lineIndex + 1))
91-
'removed-line',
92-
if (addedLines.contains(lineIndex + 1)) 'added-line',
93-
].toClasses,
94-
attributes: {
95-
if (showLineNumbers)
96-
'data-line': '${initialLineNumber + lineIndex}',
97-
},
98-
[
99-
switch (content[lineIndex]) {
100-
// Add a zero-width space when empty
101-
// so that the line isn't collapsed to 0 height.
102-
final line when line.isEmpty => span(
103-
styles: const Styles(
104-
userSelect: UserSelect.none,
105-
),
106-
[text('\u200b')],
107-
),
108-
final lineSpans => span(lineSpans),
109-
},
110-
text('\n'),
111-
],
112-
),
113-
],
114-
),
131+
code([
132+
for (var i = 0; i < content.length; i += 1) ?children[i],
133+
]),
115134
],
116135
),
117136
if (textToCopy case final textToCopy?)
@@ -151,3 +170,5 @@ enum CodeBlockTag {
151170
_ => throw ArgumentError('Unknown tag for code blocks: $tag'),
152171
};
153172
}
173+
174+
typedef FoldingRange = ({int start, int end, int level, bool open});

site/lib/src/extensions/code_block_processor.dart

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,17 @@ final class CodeBlockProcessor implements PageExtension {
8888
);
8989
}
9090

91-
final diffResult = isDiff ? _processDiffLines(lines) : null;
92-
final linesWithDiffRemoved = diffResult?.lines ?? lines;
93-
final addedLines = diffResult?.addedLines ?? const <int>{};
94-
final removedLines = diffResult?.removedLines ?? const <int>{};
91+
final isFolding = metadata.containsKey('foldable');
92+
93+
final diffResult = isDiff
94+
? _processDiffLines(lines)
95+
: (lines: lines, addedLines: <int>{}, removedLines: <int>{});
96+
final foldingResult = isFolding
97+
? _processFoldingLines(diffResult.lines)
98+
: (lines: diffResult.lines, foldingRanges: <FoldingRange>[]);
9599

96100
final codeLines = _removeHighlights(
97-
linesWithDiffRemoved,
101+
foldingResult.lines,
98102
skipHighlighting,
99103
);
100104
final processedContent = _highlightCode(
@@ -117,8 +121,9 @@ final class CodeBlockProcessor implements PageExtension {
117121
},
118122
title: title,
119123
highlightLines: _parseNumbersAndRanges(rawHighlightLines),
120-
addedLines: addedLines,
121-
removedLines: removedLines,
124+
addedLines: diffResult.addedLines,
125+
removedLines: diffResult.removedLines,
126+
foldingRanges: foldingResult.foldingRanges,
122127
tag: tag != null ? CodeBlockTag.parse(tag) : null,
123128
initialLineNumber: initialLineNumber ?? 1,
124129
showLineNumbers: showLineNumbers,
@@ -441,6 +446,65 @@ final class CodeBlockProcessor implements PageExtension {
441446
removedLines: removedLines,
442447
);
443448
}
449+
450+
/// Processes lines for folding mode, extracting folding range line markers.
451+
///
452+
/// Lines equal to '[*' are marked as the start of an open folding range.
453+
/// Lines equal to '[* -' are marked as the start of a closed folding range.
454+
/// Lines equal to '*]' are marked as the end of a folding range.
455+
/// The folding markers are removed from the lines.
456+
({
457+
List<String> lines,
458+
List<FoldingRange> foldingRanges,
459+
})
460+
_processFoldingLines(List<String> lines) {
461+
final foldingRanges = <FoldingRange>[];
462+
final processedLines = <String>[];
463+
final foldingStack = <({int start, bool open})>[];
464+
465+
for (var lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
466+
final line = lines[lineIndex];
467+
468+
// To account for removal of the folding marker lines.
469+
final lineIndexCorrection =
470+
(foldingRanges.length * 2) + foldingStack.length;
471+
472+
if (line.trim() == '[*') {
473+
foldingStack.add((
474+
start: lineIndex + 1 - lineIndexCorrection,
475+
open: true,
476+
));
477+
// Skip adding this line to processed lines.
478+
continue;
479+
} else if (line.trim() == '[* -') {
480+
foldingStack.add((
481+
start: lineIndex + 1 - lineIndexCorrection,
482+
open: false,
483+
));
484+
// Skip adding this line to processed lines.
485+
continue;
486+
} else if (line.trim() == '*]') {
487+
if (foldingStack.isNotEmpty) {
488+
final (:start, :open) = foldingStack.removeLast();
489+
foldingRanges.add((
490+
start: start,
491+
end: lineIndex - lineIndexCorrection,
492+
level: foldingStack.length,
493+
open: open,
494+
));
495+
}
496+
// Skip adding this line to processed lines.
497+
continue;
498+
}
499+
500+
processedLines.add(line);
501+
}
502+
503+
return (
504+
lines: processedLines,
505+
foldingRanges: foldingRanges,
506+
);
507+
}
444508
}
445509

446510
@immutable
@@ -451,6 +515,7 @@ final class _CodeLine {
451515
const _CodeLine({required this.content, required this.highlights});
452516
}
453517

518+
454519
extension on List<_CodeLine> {
455520
static final RegExp _terminalReplacementPattern = RegExp(
456521
r'^(\s*\$\s*)|(PS\s+)?(C:\\(.*)>\s*)',

site/lib/src/style_hash.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
// dart format off
33

44
/// The generated hash of the `main.css` file.
5-
const generatedStylesHash = 'URzUaI467vwY';
5+
const generatedStylesHash = 'jj/4JCesp/95';

0 commit comments

Comments
 (0)