@@ -19,7 +19,9 @@ library;
1919
2020import 'package:collection/collection.dart' ;
2121import 'package:html/dom.dart' as html;
22+ import 'package:html/dom_parsing.dart' as dom_parsing;
2223import 'package:html/parser.dart' as html_parser;
24+ import 'package:markdown/markdown.dart' as m;
2325import 'package:pub_semver/pub_semver.dart' ;
2426
2527/// Represents the entire changelog, containing a list of releases.
@@ -101,6 +103,177 @@ class Content {
101103 if (_asNode != null ) return _asNode! ;
102104 return html_parser.parseFragment (_asText! );
103105 }();
106+
107+ late final asMarkdownText = () {
108+ final visitor = _MarkdownVisitor ()..visit (asHtmlNode);
109+ return visitor.toString ();
110+ }();
111+ }
112+
113+ class _MarkdownVisitor extends dom_parsing.TreeVisitor {
114+ final _result = StringBuffer ();
115+ int _listDepth = 0 ;
116+
117+ void _write (String text) {
118+ _result.write (text);
119+ }
120+
121+ void _writeln ([String ? text]) {
122+ if (text != null ) {
123+ _write (text);
124+ }
125+ _write ('\n ' );
126+ }
127+
128+ void _visitChildrenInline (html.Element node) {
129+ for (var i = 0 ; i < node.nodes.length; i++ ) {
130+ final child = node.nodes[i];
131+ if (i > 0 && (node.nodes[i - 1 ].text? .endsWithWhitespace () ?? false )) {
132+ _result.write (' ' );
133+ }
134+ visit (child);
135+ }
136+ }
137+
138+ @override
139+ void visitElement (html.Element node) {
140+ final localName = node.localName! ;
141+
142+ switch (localName) {
143+ case 'h1' :
144+ _write ('# ' );
145+ _visitChildrenInline (node);
146+ _writeln ();
147+ _writeln ();
148+ break ;
149+ case 'h2' :
150+ _write ('## ' );
151+ _visitChildrenInline (node);
152+ _writeln ();
153+ _writeln ();
154+ break ;
155+ case 'h3' :
156+ _write ('### ' );
157+ _visitChildrenInline (node);
158+ _writeln ();
159+ _writeln ();
160+ break ;
161+ case 'h4' :
162+ _write ('#### ' );
163+ _visitChildrenInline (node);
164+ _writeln ();
165+ _writeln ();
166+ break ;
167+ case 'h5' :
168+ _write ('##### ' );
169+ _visitChildrenInline (node);
170+ _writeln ();
171+ _writeln ();
172+ break ;
173+ case 'h6' :
174+ _write ('###### ' );
175+ _visitChildrenInline (node);
176+ _writeln ();
177+ _writeln ();
178+ break ;
179+ case 'p' :
180+ _visitChildrenInline (node);
181+ _writeln ();
182+ _writeln ();
183+ break ;
184+ case 'br' :
185+ _writeln ();
186+ break ;
187+ case 'strong' :
188+ case 'b' :
189+ _write ('**' );
190+ _visitChildrenInline (node);
191+ _write ('**' );
192+ break ;
193+ case 'em' :
194+ case 'i' :
195+ _write ('*' );
196+ _visitChildrenInline (node);
197+ _write ('*' );
198+ break ;
199+ case 'code' :
200+ _write ('`' );
201+ _visitChildrenInline (node);
202+ _write ('`' );
203+ break ;
204+ case 'pre' :
205+ _writeln ('```' );
206+ visitChildren (node);
207+ _writeln ('```' );
208+ break ;
209+ case 'blockquote' :
210+ _write ('>' );
211+ _visitChildrenInline (node);
212+ _writeln ();
213+ break ;
214+ case 'a' :
215+ final href = node.attributes['href' ];
216+ if (href != null ) {
217+ _write ('[' );
218+ _visitChildrenInline (node);
219+ _write (']($href )' );
220+ } else {
221+ visitChildren (node);
222+ }
223+ break ;
224+ case 'ul' :
225+ _listDepth++ ;
226+ visitChildren (node);
227+ _listDepth-- ;
228+ if (_listDepth == 0 ) _writeln ();
229+ break ;
230+ case 'ol' :
231+ _listDepth++ ;
232+ visitChildren (node);
233+ _listDepth-- ;
234+ if (_listDepth == 0 ) _writeln ();
235+ break ;
236+ case 'li' :
237+ final parent = node.parent? .localName;
238+ final indent = ' ' * (_listDepth - 1 );
239+ _write (indent);
240+ if (parent == 'ol' ) {
241+ final childIndex = (node.parent? .children.indexOf (node) ?? 0 ) + 1 ;
242+ _write ('$childIndex . ' );
243+ } else {
244+ _write ('- ' );
245+ }
246+ _visitChildrenInline (node);
247+ _writeln ();
248+ break ;
249+ case 'hr' :
250+ _writeln ('---' );
251+ break ;
252+ default :
253+ visitChildren (node);
254+ break ;
255+ }
256+ }
257+
258+ @override
259+ void visitText (html.Text node) {
260+ _result.write (node.text.normalizeAndTrim ());
261+ }
262+
263+ @override
264+ String toString () => _result.toString ().trim ();
265+ }
266+
267+ extension on String {
268+ String normalizeAndTrim () {
269+ return replaceAll (RegExp (r'\s+' ), ' ' ).trim ();
270+ }
271+
272+ bool endsWithWhitespace () {
273+ if (isEmpty) return false ;
274+ final last = this [length - 1 ];
275+ return last == ' ' || last == '\n ' ;
276+ }
104277}
105278
106279/// Parses the changelog with pre-configured options.
@@ -115,7 +288,16 @@ class ChangelogParser {
115288 }) : _strictLevels = strictLevels,
116289 _partOfLevelThreshold = partOfLevelThreshold;
117290
118- /// Parses markdown nodes into a [Changelog] structure.
291+ /// Parses markdown text into a [Changelog] structure.
292+ Changelog parseMarkdownText (String input) {
293+ final nodes =
294+ m.Document (extensionSet: m.ExtensionSet .gitHubWeb).parse (input);
295+ final rawHtml = m.renderToHtml (nodes);
296+ final root = html_parser.parseFragment (rawHtml);
297+ return parseHtmlNodes (root.nodes);
298+ }
299+
300+ /// Parses HTML nodes into a [Changelog] structure.
119301 Changelog parseHtmlNodes (List <html.Node > input) {
120302 String ? title;
121303 Content ? description;
0 commit comments