Showing
2 changed files
with
434 additions
and
4 deletions
test/github_social_preview.dart
0 → 100644
| 1 | +import 'dart:io'; | ||
| 2 | + | ||
| 3 | +import 'package:pdf/pdf.dart'; | ||
| 4 | +import 'package:pdf/widgets.dart'; | ||
| 5 | +import 'package:string_scanner/string_scanner.dart'; | ||
| 6 | + | ||
| 7 | +const dpi = 72.0; | ||
| 8 | +const px = dpi / PdfPageFormat.inch * PdfPageFormat.point; | ||
| 9 | + | ||
| 10 | +void main() { | ||
| 11 | + // Open self | ||
| 12 | + final source = File('../test/github_social_preview.dart').readAsStringSync(); | ||
| 13 | + final code = DartSyntaxHighlighter( | ||
| 14 | + SyntaxHighlighterStyle.dark(), | ||
| 15 | + ).format( | ||
| 16 | + source.substring( | ||
| 17 | + source.lastIndexOf('// START') + 9, | ||
| 18 | + source.lastIndexOf('// END') - 3, | ||
| 19 | + ), | ||
| 20 | + ); | ||
| 21 | + | ||
| 22 | + // START | ||
| 23 | + // Enable debug painting | ||
| 24 | + Document.debug = true; | ||
| 25 | + | ||
| 26 | + // Create a pdf document | ||
| 27 | + final pdf = Document( | ||
| 28 | + title: 'Github Social Preview', | ||
| 29 | + author: 'David PHAM-VAN', | ||
| 30 | + ); | ||
| 31 | + | ||
| 32 | + // New page | ||
| 33 | + pdf.addPage( | ||
| 34 | + Page( | ||
| 35 | + pageFormat: PdfPageFormat( | ||
| 36 | + 1280 * px, | ||
| 37 | + 640 * px, | ||
| 38 | + marginAll: 78 * px, | ||
| 39 | + ), | ||
| 40 | + build: (context) => Row( | ||
| 41 | + mainAxisSize: MainAxisSize.max, | ||
| 42 | + children: [ | ||
| 43 | + // Display the source code on the first half of the page | ||
| 44 | + Flexible( | ||
| 45 | + fit: FlexFit.tight, | ||
| 46 | + child: Container( | ||
| 47 | + color: PdfColors.grey800, | ||
| 48 | + padding: EdgeInsets.all(10 * px), | ||
| 49 | + child: ClipRect(child: RichText(text: code)), | ||
| 50 | + ), | ||
| 51 | + ), | ||
| 52 | + // Add a vertical separator | ||
| 53 | + Container(width: 5 * px, color: PdfColors.lightBlue), | ||
| 54 | + // Show "Hello World!" centered and rotated on the second half of the page | ||
| 55 | + Flexible( | ||
| 56 | + fit: FlexFit.tight, | ||
| 57 | + child: Center( | ||
| 58 | + child: Transform.rotateBox( | ||
| 59 | + angle: .2, | ||
| 60 | + child: Text( | ||
| 61 | + 'Hello World!', | ||
| 62 | + style: TextStyle( | ||
| 63 | + font: Font.helveticaBold(), | ||
| 64 | + fontSize: 50 * px, | ||
| 65 | + ), | ||
| 66 | + ), | ||
| 67 | + ), | ||
| 68 | + ), | ||
| 69 | + ), | ||
| 70 | + ], | ||
| 71 | + ), | ||
| 72 | + ), | ||
| 73 | + ); | ||
| 74 | + // END | ||
| 75 | + | ||
| 76 | + // Save the file | ||
| 77 | + File('social_preview.pdf').writeAsBytesSync(pdf.save()); | ||
| 78 | + | ||
| 79 | + // Convert to png | ||
| 80 | + Process.runSync('pdftocairo', | ||
| 81 | + ['social_preview.pdf', '-png', '-r', '72', 'social_preview.png']); | ||
| 82 | +} | ||
| 83 | + | ||
| 84 | +class SyntaxHighlighterStyle { | ||
| 85 | + const SyntaxHighlighterStyle( | ||
| 86 | + {this.baseStyle, | ||
| 87 | + this.numberStyle, | ||
| 88 | + this.commentStyle, | ||
| 89 | + this.keywordStyle, | ||
| 90 | + this.stringStyle, | ||
| 91 | + this.punctuationStyle, | ||
| 92 | + this.classStyle, | ||
| 93 | + this.constantStyle}); | ||
| 94 | + | ||
| 95 | + final TextStyle baseStyle; | ||
| 96 | + final TextStyle numberStyle; | ||
| 97 | + final TextStyle commentStyle; | ||
| 98 | + final TextStyle keywordStyle; | ||
| 99 | + final TextStyle stringStyle; | ||
| 100 | + final TextStyle punctuationStyle; | ||
| 101 | + final TextStyle classStyle; | ||
| 102 | + final TextStyle constantStyle; | ||
| 103 | + | ||
| 104 | + factory SyntaxHighlighterStyle.dark() => SyntaxHighlighterStyle( | ||
| 105 | + baseStyle: TextStyle( | ||
| 106 | + font: Font.courierBold(), | ||
| 107 | + color: PdfColors.white, | ||
| 108 | + fontSize: 10 * px, | ||
| 109 | + ), | ||
| 110 | + numberStyle: TextStyle(color: PdfColors.purple300), | ||
| 111 | + commentStyle: TextStyle(color: PdfColors.green600), | ||
| 112 | + keywordStyle: TextStyle(color: PdfColors.blue600), | ||
| 113 | + stringStyle: TextStyle(color: PdfColors.orange400), | ||
| 114 | + punctuationStyle: TextStyle(color: PdfColors.pink), | ||
| 115 | + classStyle: TextStyle(color: PdfColors.cyan), | ||
| 116 | + constantStyle: TextStyle(color: PdfColors.pink), | ||
| 117 | + ); | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | +class DartSyntaxHighlighter { | ||
| 121 | + DartSyntaxHighlighter(this._style) { | ||
| 122 | + _spans = <_HighlightSpan>[]; | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + final SyntaxHighlighterStyle _style; | ||
| 126 | + | ||
| 127 | + static const List<String> _keywords = <String>[ | ||
| 128 | + 'abstract', | ||
| 129 | + 'as', | ||
| 130 | + 'assert', | ||
| 131 | + 'async', | ||
| 132 | + 'await', | ||
| 133 | + 'break', | ||
| 134 | + 'case', | ||
| 135 | + 'catch', | ||
| 136 | + 'class', | ||
| 137 | + 'const', | ||
| 138 | + 'continue', | ||
| 139 | + 'default', | ||
| 140 | + 'deferred', | ||
| 141 | + 'do', | ||
| 142 | + 'dynamic', | ||
| 143 | + 'else', | ||
| 144 | + 'enum', | ||
| 145 | + 'export', | ||
| 146 | + 'external', | ||
| 147 | + 'extends', | ||
| 148 | + 'factory', | ||
| 149 | + 'false', | ||
| 150 | + 'final', | ||
| 151 | + 'finally', | ||
| 152 | + 'for', | ||
| 153 | + 'get', | ||
| 154 | + 'if', | ||
| 155 | + 'implements', | ||
| 156 | + 'import', | ||
| 157 | + 'in', | ||
| 158 | + 'is', | ||
| 159 | + 'library', | ||
| 160 | + 'new', | ||
| 161 | + 'null', | ||
| 162 | + 'operator', | ||
| 163 | + 'part', | ||
| 164 | + 'rethrow', | ||
| 165 | + 'return', | ||
| 166 | + 'set', | ||
| 167 | + 'static', | ||
| 168 | + 'super', | ||
| 169 | + 'switch', | ||
| 170 | + 'sync', | ||
| 171 | + 'this', | ||
| 172 | + 'throw', | ||
| 173 | + 'true', | ||
| 174 | + 'try', | ||
| 175 | + 'typedef', | ||
| 176 | + 'var', | ||
| 177 | + 'void', | ||
| 178 | + 'while', | ||
| 179 | + 'with', | ||
| 180 | + 'yield' | ||
| 181 | + ]; | ||
| 182 | + | ||
| 183 | + static const List<String> _builtInTypes = <String>[ | ||
| 184 | + 'int', | ||
| 185 | + 'double', | ||
| 186 | + 'num', | ||
| 187 | + 'bool' | ||
| 188 | + ]; | ||
| 189 | + | ||
| 190 | + String _src; | ||
| 191 | + StringScanner _scanner; | ||
| 192 | + | ||
| 193 | + List<_HighlightSpan> _spans; | ||
| 194 | + | ||
| 195 | + TextSpan format(String source) { | ||
| 196 | + _src = source; | ||
| 197 | + | ||
| 198 | + _scanner = StringScanner(_src); | ||
| 199 | + | ||
| 200 | + if (_generateSpans()) { | ||
| 201 | + // Successfully parsed the code | ||
| 202 | + final List<TextSpan> formattedText = <TextSpan>[]; | ||
| 203 | + int currentPosition = 0; | ||
| 204 | + | ||
| 205 | + for (_HighlightSpan span in _spans) { | ||
| 206 | + if (currentPosition != span.start) | ||
| 207 | + formattedText | ||
| 208 | + .add(TextSpan(text: _src.substring(currentPosition, span.start))); | ||
| 209 | + | ||
| 210 | + formattedText.add(TextSpan( | ||
| 211 | + style: span.textStyle(_style), text: span.textForSpan(_src))); | ||
| 212 | + | ||
| 213 | + currentPosition = span.end; | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + if (currentPosition != _src.length) | ||
| 217 | + formattedText | ||
| 218 | + .add(TextSpan(text: _src.substring(currentPosition, _src.length))); | ||
| 219 | + _spans.clear(); | ||
| 220 | + return TextSpan(style: _style.baseStyle, children: formattedText); | ||
| 221 | + } else { | ||
| 222 | + // Parsing failed, return with only basic formatting | ||
| 223 | + return TextSpan(style: _style.baseStyle, text: source); | ||
| 224 | + } | ||
| 225 | + } | ||
| 226 | + | ||
| 227 | + bool _generateSpans() { | ||
| 228 | + int lastLoopPosition = _scanner.position; | ||
| 229 | + | ||
| 230 | + while (!_scanner.isDone) { | ||
| 231 | + // Skip White space | ||
| 232 | + _scanner.scan(RegExp(r'\s+')); | ||
| 233 | + | ||
| 234 | + // Block comments | ||
| 235 | + if (_scanner.scan(RegExp(r'/\*(.|\n)*\*/'))) { | ||
| 236 | + _spans.add(_HighlightSpan(_HighlightType.comment, | ||
| 237 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 238 | + continue; | ||
| 239 | + } | ||
| 240 | + | ||
| 241 | + // Line comments | ||
| 242 | + if (_scanner.scan('//')) { | ||
| 243 | + final int startComment = _scanner.lastMatch.start; | ||
| 244 | + | ||
| 245 | + bool eof = false; | ||
| 246 | + int endComment; | ||
| 247 | + if (_scanner.scan(RegExp(r'.*\n'))) { | ||
| 248 | + endComment = _scanner.lastMatch.end - 1; | ||
| 249 | + } else { | ||
| 250 | + eof = true; | ||
| 251 | + endComment = _src.length; | ||
| 252 | + } | ||
| 253 | + | ||
| 254 | + _spans.add( | ||
| 255 | + _HighlightSpan(_HighlightType.comment, startComment, endComment)); | ||
| 256 | + | ||
| 257 | + if (eof) { | ||
| 258 | + break; | ||
| 259 | + } | ||
| 260 | + | ||
| 261 | + continue; | ||
| 262 | + } | ||
| 263 | + | ||
| 264 | + // Raw r"String" | ||
| 265 | + if (_scanner.scan(RegExp(r'r".*"'))) { | ||
| 266 | + _spans.add(_HighlightSpan(_HighlightType.string, | ||
| 267 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 268 | + continue; | ||
| 269 | + } | ||
| 270 | + | ||
| 271 | + // Raw r'String' | ||
| 272 | + if (_scanner.scan(RegExp(r"r'.*'"))) { | ||
| 273 | + _spans.add(_HighlightSpan(_HighlightType.string, | ||
| 274 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 275 | + continue; | ||
| 276 | + } | ||
| 277 | + | ||
| 278 | + // Multiline """String""" | ||
| 279 | + if (_scanner.scan(RegExp(r'"""(?:[^"\\]|\\(.|\n))*"""'))) { | ||
| 280 | + _spans.add(_HighlightSpan(_HighlightType.string, | ||
| 281 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 282 | + continue; | ||
| 283 | + } | ||
| 284 | + | ||
| 285 | + // Multiline '''String''' | ||
| 286 | + if (_scanner.scan(RegExp(r"'''(?:[^'\\]|\\(.|\n))*'''"))) { | ||
| 287 | + _spans.add(_HighlightSpan(_HighlightType.string, | ||
| 288 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 289 | + continue; | ||
| 290 | + } | ||
| 291 | + | ||
| 292 | + // "String" | ||
| 293 | + if (_scanner.scan(RegExp(r'"(?:[^"\\]|\\.)*"'))) { | ||
| 294 | + _spans.add(_HighlightSpan(_HighlightType.string, | ||
| 295 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 296 | + continue; | ||
| 297 | + } | ||
| 298 | + | ||
| 299 | + // 'String' | ||
| 300 | + if (_scanner.scan(RegExp(r"'(?:[^'\\]|\\.)*'"))) { | ||
| 301 | + _spans.add(_HighlightSpan(_HighlightType.string, | ||
| 302 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 303 | + continue; | ||
| 304 | + } | ||
| 305 | + | ||
| 306 | + // Double | ||
| 307 | + if (_scanner.scan(RegExp(r'\d+\.\d+'))) { | ||
| 308 | + _spans.add(_HighlightSpan(_HighlightType.number, | ||
| 309 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 310 | + continue; | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + // Integer | ||
| 314 | + if (_scanner.scan(RegExp(r'\d+'))) { | ||
| 315 | + _spans.add(_HighlightSpan(_HighlightType.number, | ||
| 316 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 317 | + continue; | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + // Punctuation | ||
| 321 | + if (_scanner.scan(RegExp(r'[\[\]{}().!=<>&\|\?\+\-\*/%\^~;:,]'))) { | ||
| 322 | + _spans.add(_HighlightSpan(_HighlightType.punctuation, | ||
| 323 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 324 | + continue; | ||
| 325 | + } | ||
| 326 | + | ||
| 327 | + // Meta data | ||
| 328 | + if (_scanner.scan(RegExp(r'@\w+'))) { | ||
| 329 | + _spans.add(_HighlightSpan(_HighlightType.keyword, | ||
| 330 | + _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 331 | + continue; | ||
| 332 | + } | ||
| 333 | + | ||
| 334 | + // Words | ||
| 335 | + if (_scanner.scan(RegExp(r'\w+'))) { | ||
| 336 | + _HighlightType type; | ||
| 337 | + | ||
| 338 | + String word = _scanner.lastMatch[0]; | ||
| 339 | + if (word.startsWith('_')) { | ||
| 340 | + word = word.substring(1); | ||
| 341 | + } | ||
| 342 | + | ||
| 343 | + if (_keywords.contains(word)) | ||
| 344 | + type = _HighlightType.keyword; | ||
| 345 | + else if (_builtInTypes.contains(word)) | ||
| 346 | + type = _HighlightType.keyword; | ||
| 347 | + else if (_firstLetterIsUpperCase(word)) | ||
| 348 | + type = _HighlightType.klass; | ||
| 349 | + else if (word.length >= 2 && | ||
| 350 | + word.startsWith('k') && | ||
| 351 | + _firstLetterIsUpperCase(word.substring(1))) | ||
| 352 | + type = _HighlightType.constant; | ||
| 353 | + | ||
| 354 | + if (type != null) { | ||
| 355 | + _spans.add(_HighlightSpan( | ||
| 356 | + type, _scanner.lastMatch.start, _scanner.lastMatch.end)); | ||
| 357 | + } | ||
| 358 | + } | ||
| 359 | + | ||
| 360 | + // Check if this loop did anything | ||
| 361 | + if (lastLoopPosition == _scanner.position) { | ||
| 362 | + // Failed to parse this file, abort gracefully | ||
| 363 | + return false; | ||
| 364 | + } | ||
| 365 | + lastLoopPosition = _scanner.position; | ||
| 366 | + } | ||
| 367 | + | ||
| 368 | + _simplify(); | ||
| 369 | + return true; | ||
| 370 | + } | ||
| 371 | + | ||
| 372 | + void _simplify() { | ||
| 373 | + for (int i = _spans.length - 2; i >= 0; i -= 1) { | ||
| 374 | + if (_spans[i].type == _spans[i + 1].type && | ||
| 375 | + _spans[i].end == _spans[i + 1].start) { | ||
| 376 | + _spans[i] = | ||
| 377 | + _HighlightSpan(_spans[i].type, _spans[i].start, _spans[i + 1].end); | ||
| 378 | + _spans.removeAt(i + 1); | ||
| 379 | + } | ||
| 380 | + } | ||
| 381 | + } | ||
| 382 | + | ||
| 383 | + bool _firstLetterIsUpperCase(String str) { | ||
| 384 | + if (str.isNotEmpty) { | ||
| 385 | + final String first = str.substring(0, 1); | ||
| 386 | + return first == first.toUpperCase(); | ||
| 387 | + } | ||
| 388 | + return false; | ||
| 389 | + } | ||
| 390 | +} | ||
| 391 | + | ||
| 392 | +enum _HighlightType { | ||
| 393 | + number, | ||
| 394 | + comment, | ||
| 395 | + keyword, | ||
| 396 | + string, | ||
| 397 | + punctuation, | ||
| 398 | + klass, | ||
| 399 | + constant | ||
| 400 | +} | ||
| 401 | + | ||
| 402 | +class _HighlightSpan { | ||
| 403 | + _HighlightSpan(this.type, this.start, this.end); | ||
| 404 | + final _HighlightType type; | ||
| 405 | + final int start; | ||
| 406 | + final int end; | ||
| 407 | + | ||
| 408 | + String textForSpan(String src) { | ||
| 409 | + return src.substring(start, end); | ||
| 410 | + } | ||
| 411 | + | ||
| 412 | + TextStyle textStyle(SyntaxHighlighterStyle style) { | ||
| 413 | + if (type == _HighlightType.number) | ||
| 414 | + return style.numberStyle; | ||
| 415 | + else if (type == _HighlightType.comment) | ||
| 416 | + return style.commentStyle; | ||
| 417 | + else if (type == _HighlightType.keyword) | ||
| 418 | + return style.keywordStyle; | ||
| 419 | + else if (type == _HighlightType.string) | ||
| 420 | + return style.stringStyle; | ||
| 421 | + else if (type == _HighlightType.punctuation) | ||
| 422 | + return style.punctuationStyle; | ||
| 423 | + else if (type == _HighlightType.klass) | ||
| 424 | + return style.classStyle; | ||
| 425 | + else if (type == _HighlightType.constant) | ||
| 426 | + return style.constantStyle; | ||
| 427 | + else | ||
| 428 | + return style.baseStyle; | ||
| 429 | + } | ||
| 430 | +} |
| @@ -10,15 +10,15 @@ environment: | @@ -10,15 +10,15 @@ environment: | ||
| 10 | sdk: ">=2.1.0 <3.0.0" | 10 | sdk: ">=2.1.0 <3.0.0" |
| 11 | flutter: "^1.2.0" | 11 | flutter: "^1.2.0" |
| 12 | 12 | ||
| 13 | -dependencies: | 13 | +dependencies: |
| 14 | flutter: | 14 | flutter: |
| 15 | sdk: flutter | 15 | sdk: flutter |
| 16 | markdown: | 16 | markdown: |
| 17 | - meta: | ||
| 18 | - string_scanner: | 17 | + meta: |
| 18 | + string_scanner: | ||
| 19 | pdf: | 19 | pdf: |
| 20 | printing: | 20 | printing: |
| 21 | - path: | 21 | + path: |
| 22 | image: | 22 | image: |
| 23 | path_provider: | 23 | path_provider: |
| 24 | 24 |
-
Please register or login to post a comment