Skip to content

Commit e723d6a

Browse files
authored
New changelog page. (#3327)
* New changelog page. * Catching only FormatException * h2 in documentation * Return Version instead of String in _extractVersion
1 parent bd696e6 commit e723d6a

File tree

5 files changed

+182
-12
lines changed

5 files changed

+182
-12
lines changed

app/lib/frontend/templates/_utils.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@ import '../../shared/markdown.dart';
1212
const HtmlEscape htmlAttrEscape = HtmlEscape(HtmlEscapeMode.attribute);
1313

1414
/// Renders a file content (e.g. markdown, dart source file) into HTML.
15-
String renderFile(FileObject file, String baseUrl) {
15+
String renderFile(
16+
FileObject file,
17+
String baseUrl, {
18+
bool isChangelog = false,
19+
}) {
1620
final filename = file.filename;
1721
final content = file.text;
1822
if (content != null) {
1923
if (_isMarkdownFile(filename)) {
20-
return markdownToHtml(content, baseUrl, baseDir: p.dirname(filename));
24+
return markdownToHtml(content, baseUrl,
25+
baseDir: p.dirname(filename), isChangelog: isChangelog);
2126
} else if (_isDartFile(filename)) {
2227
return _renderDartCode(content);
2328
} else {

app/lib/frontend/templates/package.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import '../../shared/email.dart' show EmailAddress;
1616
import '../../shared/tags.dart';
1717
import '../../shared/urls.dart' as urls;
1818

19+
import '../request_context.dart';
1920
import '../static_files.dart';
2021

2122
import '_cache.dart';
@@ -310,7 +311,8 @@ List<Tab> _pkgTabs(
310311

311312
String renderedChangelog;
312313
if (selectedVersion.changelog != null) {
313-
renderedChangelog = renderFile(selectedVersion.changelog, baseUrl);
314+
renderedChangelog = renderFile(selectedVersion.changelog, baseUrl,
315+
isChangelog: requestContext.isExperimental);
314316
}
315317

316318
String renderedExample;

app/lib/shared/markdown.dart

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,26 @@ import 'package:logging/logging.dart';
66
import 'package:markdown/markdown.dart' as m;
77
import 'package:pana/pana.dart' show getRepositoryUrl;
88
import 'package:path/path.dart' as p;
9+
import 'package:pub_semver/pub_semver.dart';
910
import 'package:sanitize_html/sanitize_html.dart';
1011

1112
final Logger _logger = Logger('pub.markdown');
1213

1314
const _whitelistedClassNames = <String>[
15+
'changelog-entry',
16+
'changelog-version',
17+
'changelog-content',
1418
'hash-header',
1519
'hash-link',
1620
];
1721

18-
String markdownToHtml(String text, String baseUrl, {String baseDir}) {
22+
/// Renders markdown [text] to HTML.
23+
String markdownToHtml(
24+
String text,
25+
String baseUrl, {
26+
String baseDir,
27+
bool isChangelog = false,
28+
}) {
1929
if (text == null) return null;
2030
final sanitizedBaseUrl = _pruneBaseUrl(baseUrl);
2131

@@ -24,7 +34,11 @@ String markdownToHtml(String text, String baseUrl, {String baseDir}) {
2434
blockSyntaxes: m.ExtensionSet.gitHubWeb.blockSyntaxes);
2535

2636
final lines = text.replaceAll('\r\n', '\n').split('\n');
27-
final nodes = document.parseLines(lines);
37+
var nodes = document.parseLines(lines);
38+
39+
if (isChangelog) {
40+
nodes = _groupChangelogNodes(nodes).toList();
41+
}
2842

2943
final urlRewriter = _RelativeUrlRewriter(sanitizedBaseUrl, baseDir);
3044
final hashLink = _HashLink();
@@ -63,17 +77,22 @@ class _HashLink implements m.NodeVisitor {
6377
element.children.length == 1;
6478

6579
if (isHeaderWithHash) {
66-
element.attributes['class'] = 'hash-header';
67-
element.children.addAll([
68-
m.Text(' '),
69-
m.Element('a', [m.Text('#')])
70-
..attributes['href'] = '#${element.generatedId}'
71-
..attributes['class'] = 'hash-link',
72-
]);
80+
_addHashLink(element, element.generatedId);
7381
}
7482
}
7583
}
7684

85+
void _addHashLink(m.Element element, String id) {
86+
final currentClasses = element.attributes['class'] ?? '';
87+
element.attributes['class'] = '$currentClasses hash-header'.trim();
88+
element.children.addAll([
89+
m.Text(' '),
90+
m.Element('a', [m.Text('#')])
91+
..attributes['href'] = '#$id'
92+
..attributes['class'] = 'hash-link',
93+
]);
94+
}
95+
7796
/// Filters unsafe URLs from the generated HTML.
7897
class _UnsafeUrlFilter implements m.NodeVisitor {
7998
static const _trustedSchemes = <String>['http', 'https', 'mailto'];
@@ -212,3 +231,67 @@ String _pruneBaseUrl(String url) {
212231
}
213232
return null;
214233
}
234+
235+
/// Group corresponding changelog nodes together, if it matches the following
236+
/// pattern:
237+
/// - version identifiers are the only content in a single line
238+
/// - heading level or other style doesn't matter
239+
/// - optional `v` prefix is accepted
240+
/// - message logs between identifiers are copied to the version entry before the line
241+
///
242+
/// The output is in the following structure:
243+
/// <div class="changelog-entry">
244+
/// <h2 class="changelog-version">{{version - stripped from styles}}</h2>
245+
/// <div class="changelog-content">
246+
/// {{log entries in their original HTML format}}
247+
/// </div>
248+
/// </div>
249+
Iterable<m.Node> _groupChangelogNodes(List<m.Node> nodes) sync* {
250+
m.Element lastContentDiv;
251+
for (final node in nodes) {
252+
final version = (node is m.Element &&
253+
node.children.isNotEmpty &&
254+
node.children.first is m.Text)
255+
? _extractVersion(node.children.first.textContent)
256+
: null;
257+
if (version != null) {
258+
final titleElem = m.Element('h2', [m.Text(version.toString())])
259+
..attributes['class'] = 'changelog-version';
260+
final generatedId = (node as m.Element).generatedId;
261+
if (generatedId != null) {
262+
titleElem.attributes['id'] = generatedId;
263+
_addHashLink(titleElem, generatedId);
264+
}
265+
266+
lastContentDiv = m.Element('div', [])
267+
..attributes['class'] = 'changelog-content';
268+
269+
yield m.Element('div', [
270+
titleElem,
271+
lastContentDiv,
272+
])
273+
..attributes['class'] = 'changelog-entry';
274+
} else if (lastContentDiv != null) {
275+
lastContentDiv.children.add(node);
276+
} else {
277+
yield node;
278+
}
279+
}
280+
}
281+
282+
/// Returns the extracted version (if it is a specific version, not `any` or empty).
283+
Version _extractVersion(String text) {
284+
if (text == null || text.isEmpty) return null;
285+
text = text.trim();
286+
if (text.startsWith('v')) {
287+
text = text.substring(1).trim();
288+
}
289+
if (text.isEmpty) return null;
290+
try {
291+
final v = Version.parse(text);
292+
if (v.isEmpty || v.isAny) return null;
293+
return v;
294+
} on FormatException catch (_) {
295+
return null;
296+
}
297+
}

app/test/shared/markdown_test.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,65 @@ void main() {
201201
'<p><img src="https://github.com/rcpassos/progress_hud/raw/master/progress_hud.gif" alt="text" /></p>\n');
202202
});
203203
});
204+
205+
group('changelog', () {
206+
test('no structure', () {
207+
expect(
208+
markdownToHtml(
209+
'a\n\n'
210+
'b\n\n'
211+
'c',
212+
null,
213+
isChangelog: true),
214+
'<p>a</p>\n'
215+
'<p>b</p>\n'
216+
'<p>c</p>\n');
217+
});
218+
219+
test('single entry', () {
220+
expect(
221+
markdownToHtml(
222+
'# Changelog\n\n'
223+
'## 1.0.0\n'
224+
'\n'
225+
'- change1',
226+
null,
227+
isChangelog: true),
228+
'<h1 class="hash-header" id="changelog">Changelog <a href="#changelog" class="hash-link">#</a></h1>'
229+
'<div class="changelog-entry">\n'
230+
'<h2 class="changelog-version hash-header" id="100">1.0.0 <a href="#100" class="hash-link">#</a></h2>'
231+
'<div class="changelog-content">\n'
232+
'<ul>\n'
233+
'<li>change1</li>\n'
234+
'</ul>'
235+
'</div>'
236+
'</div>\n');
237+
});
238+
239+
test('multiple entries', () {
240+
expect(
241+
markdownToHtml(
242+
'# Changelog\n\n'
243+
'## 1.0.0\n\n- change1\n\n- change2\n\n'
244+
'## 0.9.0\n\nMostly refatoring',
245+
null,
246+
isChangelog: true),
247+
'<h1 class="hash-header" id="changelog">Changelog <a href="#changelog" class="hash-link">#</a></h1>'
248+
'<div class="changelog-entry">\n'
249+
'<h2 class="changelog-version hash-header" id="100">1.0.0 <a href="#100" class="hash-link">#</a></h2>'
250+
'<div class="changelog-content">\n'
251+
'<ul>\n'
252+
'<li>\n<p>change1</p>\n</li>\n'
253+
'<li>\n<p>change2</p>\n</li>\n'
254+
'</ul>'
255+
'</div>'
256+
'</div>'
257+
'<div class="changelog-entry">\n'
258+
'<h2 class="changelog-version hash-header" id="090">0.9.0 <a href="#090" class="hash-link">#</a></h2>'
259+
'<div class="changelog-content">\n'
260+
'<p>Mostly refatoring</p>'
261+
'</div>'
262+
'</div>\n');
263+
});
264+
});
204265
}

pkg/web_css/lib/src/_pkg_experimental.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,24 @@ body.experimental {
6767
}
6868
}
6969

70+
.changelog-entry {
71+
display: flex;
72+
padding: 12px 0;
73+
border-bottom: 1px solid #c8c8ca;
74+
75+
.changelog-version {
76+
border-bottom: none;
77+
margin: 0;
78+
width: 120px;
79+
}
80+
81+
.changelog-content {
82+
flex-grow: 1;
83+
font-size: 14px;
84+
margin: 4px 0 4px 16px;
85+
width: 100%;
86+
}
87+
}
88+
7089
/* non-indented rule to restrict the content of the file to the experimental pages */
7190
}

0 commit comments

Comments
 (0)