David PHAM-VAN

Add Social Preview image

  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 +}