├── .gitignore ├── favicon.ico ├── images ├── glider.gif └── glider.4.gif ├── public ├── fonts │ ├── Hypertext4D.woff │ └── Hypertext4D.woff2 ├── html │ └── gpgc_redirect.html ├── css │ └── gpgc_styles.css └── js │ └── gpgc_core.js ├── _includes ├── icon-github.html ├── icon-twitter.html ├── icon-twitter.svg ├── icon-github.svg ├── head.html ├── footer.html ├── header.html └── gpgc_comments.html ├── css ├── title-font.css └── main.scss ├── _layouts ├── page.html ├── default.html └── post.html ├── _data └── gpgc.yml ├── index.html ├── _config.yml ├── scripts └── mouseScrollTest │ ├── scrollEventHandler.js │ └── mouseScrollTest.pde ├── feed.xml ├── about.md ├── _sass ├── _syntax-highlighting.scss ├── _base.scss └── _layout.scss ├── _posts ├── 2016-06-20-generating-fractal-glider-logo.markdown ├── 2018-02-13-testing-mouse-scroll-wheel.md └── 2016-06-27-parallel-runners-junit4.markdown └── _tools └── gpgcCreateCommentIssue.sh /.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-metadata 4 | secret 5 | 6 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractalglider/fractalglider.github.io/HEAD/favicon.ico -------------------------------------------------------------------------------- /images/glider.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractalglider/fractalglider.github.io/HEAD/images/glider.gif -------------------------------------------------------------------------------- /images/glider.4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractalglider/fractalglider.github.io/HEAD/images/glider.4.gif -------------------------------------------------------------------------------- /public/fonts/Hypertext4D.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractalglider/fractalglider.github.io/HEAD/public/fonts/Hypertext4D.woff -------------------------------------------------------------------------------- /public/fonts/Hypertext4D.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fractalglider/fractalglider.github.io/HEAD/public/fonts/Hypertext4D.woff2 -------------------------------------------------------------------------------- /_includes/icon-github.html: -------------------------------------------------------------------------------- 1 | {% include icon-github.svg %}{{ include.username }} 2 | -------------------------------------------------------------------------------- /_includes/icon-twitter.html: -------------------------------------------------------------------------------- 1 | {% include icon-twitter.svg %}{{ include.username }} 2 | -------------------------------------------------------------------------------- /css/title-font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Hypertext 4D'; 3 | src: url('/public/fonts/Hypertext4D.woff2') format('woff2'), 4 | url('/public/fonts/Hypertext4D.woff') format('woff'); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |
5 | 6 |
7 |

{{ page.title }}

8 |
9 | 10 |
11 | {{ content }} 12 |
13 | 14 |
15 | -------------------------------------------------------------------------------- /_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include head.html %} 5 | 6 | 7 | 8 | {% include header.html %} 9 | 10 |
11 |
12 | {{ content }} 13 |
14 |
15 | 16 | {% include footer.html %} 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /_data/gpgc.yml: -------------------------------------------------------------------------------- 1 | repo_owner: fractalglider 2 | repo_name: fractalglider.github.io 3 | disabled: false 4 | use_show_action: false 5 | label_name: Post Comments 6 | label_color: 666666 7 | github_application: 8 | client_id: 0912cb3ef40e0ecc7cdf 9 | code_authenticator: https://fractalglider-gatekeeper.herokuapp.com/authenticate/ 10 | callback_url: https://fractalglider.github.io/public/html/gpgc_redirect.html 11 | enable_diagnostics: false 12 | -------------------------------------------------------------------------------- /public/html/gpgc_redirect.html: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 | 7 |

Posts

8 | 9 | 20 | 21 |

subscribe via RSS

22 | 23 |
24 | -------------------------------------------------------------------------------- /_layouts/post.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 |
5 | 6 |
7 |

{{ page.title }}

8 | 9 |
10 | 11 |
12 | {{ content }} 13 |
14 | 15 | {% include gpgc_comments.html post_title=page.title %} 16 | 17 |
18 | -------------------------------------------------------------------------------- /_includes/icon-twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Jekyll! 2 | # 3 | # This config file is meant for settings that affect your whole blog, values 4 | # which you are expected to set up once and rarely need to edit after that. 5 | # For technical reasons, this file is *NOT* reloaded automatically when you use 6 | # 'jekyll serve'. If you change this file, please restart the server process. 7 | 8 | # Site settings 9 | title: Fractal glider 10 | email: fractalgliderblog (at) gmail (dot) com 11 | description: > # this means to ignore newlines until "baseurl:" 12 | Programming blog 13 | baseurl: "" # the subpath of your site, e.g. /blog 14 | url: "https://fractalglider.github.io" # the base hostname & protocol for your site 15 | twitter_username: fractalblog 16 | github_username: odisseus 17 | 18 | # Build settings 19 | markdown: kramdown 20 | -------------------------------------------------------------------------------- /scripts/mouseScrollTest/scrollEventHandler.js: -------------------------------------------------------------------------------- 1 | function handle(delta) { 2 | var event = { 3 | getCount: function() { return delta; } 4 | }; 5 | Processing.getInstanceById('sketch').mouseWheel(event); 6 | } 7 | 8 | function wheel(event){ 9 | event.preventDefault(); 10 | var delta = 0; 11 | if (!event) { 12 | event = window.event; 13 | } 14 | if (event.wheelDelta) { 15 | delta = event.wheelDelta/120; 16 | if (window.opera) { 17 | delta = -delta; 18 | } 19 | } else if (event.detail) { 20 | delta = -event.detail/3; 21 | } 22 | if (delta) { 23 | handle(delta); 24 | } 25 | return false; 26 | } 27 | 28 | function bind(obj){ 29 | if (obj.addEventListener){ 30 | obj.addEventListener('DOMMouseScroll', wheel, false); 31 | } 32 | obj.onmousewheel = wheel; 33 | } 34 | 35 | bind(document.getElementById('sketch')); 36 | -------------------------------------------------------------------------------- /_includes/icon-github.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape }}{% endif %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /_includes/footer.html: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /scripts/mouseScrollTest/mouseScrollTest.pde: -------------------------------------------------------------------------------- 1 | void setup() { 2 | size(300, 300); 3 | } 4 | 5 | // Current coordinates of the tick 6 | float x = 150; 7 | float y = 150; 8 | // Coordinates the tick had during the previous frame 9 | float prevx = 150; 10 | float prevy = 150; 11 | 12 | float tickHeight = 10; 13 | 14 | void draw() { 15 | // Draw a semi-transparent background so that the trace fades with time 16 | pushStyle(); 17 | noStroke(); 18 | fill(0, 5); 19 | rect(0, 0, width, height); 20 | popStyle(); 21 | 22 | // Draw the tick 23 | strokeWeight(2); 24 | line(prevx, prevy, x, y); 25 | 26 | // Move the rick to the right 27 | prevx = x; 28 | x = x + 1; 29 | if(x > width){ 30 | x = 0; 31 | prevx = x; 32 | } 33 | } 34 | 35 | void mouseWheel(MouseEvent event) { 36 | float notchesScrolled = event.getCount(); 37 | 38 | // Paint the tick green if the last scroll was up, red otherwise 39 | if(notchesScrolled < 0){ 40 | stroke(0, 255, 0); 41 | } else { 42 | stroke(255, 0, 0); 43 | } 44 | 45 | // Move the tick in the direction of scroll 46 | prevy = y; 47 | y = y + (notchesScrolled * tickHeight); 48 | if(y > height){ 49 | y = 0; 50 | prevy = y; 51 | } else if (y < 0){ 52 | y = height; 53 | prevy = y; 54 | } 55 | } -------------------------------------------------------------------------------- /css/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | # Only the main Sass file needs front matter (the dashes are enough) 3 | --- 4 | @charset "utf-8"; 5 | 6 | 7 | 8 | // Our variables 9 | $base-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 10 | $base-font-size: 16px; 11 | $base-font-weight: 400; 12 | $small-font-size: $base-font-size * 0.875; 13 | $base-line-height: 1.5; 14 | 15 | $spacing-unit: 30px; 16 | 17 | $text-color: #111; 18 | $background-color: #fdfdfd; 19 | $brand-color: #2a7ae2; 20 | 21 | $grey-color: #828282; 22 | $grey-color-light: lighten($grey-color, 40%); 23 | $grey-color-dark: darken($grey-color, 25%); 24 | 25 | // Width of the content area 26 | $content-width: 800px; 27 | 28 | $on-palm: 600px; 29 | $on-laptop: 800px; 30 | 31 | 32 | 33 | // Use media queries like this: 34 | // @include media-query($on-palm) { 35 | // .wrapper { 36 | // padding-right: $spacing-unit / 2; 37 | // padding-left: $spacing-unit / 2; 38 | // } 39 | // } 40 | @mixin media-query($device) { 41 | @media screen and (max-width: $device) { 42 | @content; 43 | } 44 | } 45 | 46 | 47 | 48 | // Import partials from `sass_dir` (defaults to `_sass`) 49 | @import 50 | "base", 51 | "layout", 52 | "syntax-highlighting" 53 | ; 54 | -------------------------------------------------------------------------------- /_includes/header.html: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /feed.xml: -------------------------------------------------------------------------------- 1 | --- 2 | layout: null 3 | --- 4 | 5 | 6 | 7 | {{ site.title | xml_escape }} 8 | {{ site.description | xml_escape }} 9 | {{ site.url }}{{ site.baseurl }}/ 10 | 11 | {{ site.time | date_to_rfc822 }} 12 | {{ site.time | date_to_rfc822 }} 13 | Jekyll v{{ jekyll.version }} 14 | {% for post in site.posts limit:10 %} 15 | 16 | {{ post.title | xml_escape }} 17 | {{ post.content | xml_escape }} 18 | {{ post.date | date_to_rfc822 }} 19 | {{ post.url | prepend: site.baseurl | prepend: site.url }} 20 | {{ post.url | prepend: site.baseurl | prepend: site.url }} 21 | {% for tag in post.tags %} 22 | {{ tag | xml_escape }} 23 | {% endfor %} 24 | {% for cat in post.categories %} 25 | {{ cat | xml_escape }} 26 | {% endfor %} 27 | 28 | {% endfor %} 29 | 30 | 31 | -------------------------------------------------------------------------------- /about.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: About 4 | permalink: /about/ 5 | --- 6 | 7 | # About me 8 | 9 | I am a software developer with professional experience in Java since 2012. I am interested in Scala, machine learning and Linux. 10 | 11 | My GitHub account is {% include icon-github.html username="odisseus" %}. 12 | 13 | I speak English and Ukrainian languages and, to a lesser extent, Polish and Russian. 14 | 15 | # About the blog 16 | 17 | This blog is hosted by [GitHub pages][ghpages]. Its repository is at: 18 | {% include icon-github.html username="fractalglider" %} / [fractalglider.github.io][blogrepo]. 19 | 20 | The comments are managed by a slightly customized [ghcomments][ghcomments] plugin and stored as issue comments in the blog's repository. The comment threads can be viewed as [GitHub issues][issues]. 21 | 22 | This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](http://jekyllrb.com/) 23 | 24 | You can find the source code for the Jekyll new theme at: 25 | {% include icon-github.html username="jglovier" %} / 26 | [jekyll-new](https://github.com/jglovier/jekyll-new) 27 | 28 | You can find the source code for Jekyll at 29 | {% include icon-github.html username="jekyll" %} / 30 | [jekyll](https://github.com/jekyll/jekyll) 31 | 32 | [ghpages]:https://pages.github.com/ 33 | [blogrepo]:https://github.com/fractalglider/fractalglider.github.io 34 | [ghcomments]:http://downtothewire.io/ghpages-ghcomments/about/ 35 | [issues]:https://github.com/fractalglider/fractalglider.github.io/issues 36 | 37 | -------------------------------------------------------------------------------- /_sass/_syntax-highlighting.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Syntax highlighting styles 3 | */ 4 | .highlight { 5 | background: #fff; 6 | @extend %vertical-rhythm; 7 | 8 | .highlighter-rouge & { 9 | background: #eef; 10 | } 11 | 12 | .c { color: #998; font-style: italic } // Comment 13 | .err { color: #a61717; background-color: #e3d2d2 } // Error 14 | .k { font-weight: bold } // Keyword 15 | .o { font-weight: bold } // Operator 16 | .cm { color: #998; font-style: italic } // Comment.Multiline 17 | .cp { color: #999; font-weight: bold } // Comment.Preproc 18 | .c1 { color: #998; font-style: italic } // Comment.Single 19 | .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special 20 | .gd { color: #000; background-color: #fdd } // Generic.Deleted 21 | .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific 22 | .ge { font-style: italic } // Generic.Emph 23 | .gr { color: #a00 } // Generic.Error 24 | .gh { color: #999 } // Generic.Heading 25 | .gi { color: #000; background-color: #dfd } // Generic.Inserted 26 | .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific 27 | .go { color: #888 } // Generic.Output 28 | .gp { color: #555 } // Generic.Prompt 29 | .gs { font-weight: bold } // Generic.Strong 30 | .gu { color: #aaa } // Generic.Subheading 31 | .gt { color: #a00 } // Generic.Traceback 32 | .kc { font-weight: bold } // Keyword.Constant 33 | .kd { font-weight: bold } // Keyword.Declaration 34 | .kp { font-weight: bold } // Keyword.Pseudo 35 | .kr { font-weight: bold } // Keyword.Reserved 36 | .kt { color: #458; font-weight: bold } // Keyword.Type 37 | .m { color: #099 } // Literal.Number 38 | .s { color: #d14 } // Literal.String 39 | .na { color: #008080 } // Name.Attribute 40 | .nb { color: #0086B3 } // Name.Builtin 41 | .nc { color: #458; font-weight: bold } // Name.Class 42 | .no { color: #008080 } // Name.Constant 43 | .ni { color: #800080 } // Name.Entity 44 | .ne { color: #900; font-weight: bold } // Name.Exception 45 | .nf { color: #900; font-weight: bold } // Name.Function 46 | .nn { color: #555 } // Name.Namespace 47 | .nt { color: #000080 } // Name.Tag 48 | .nv { color: #008080 } // Name.Variable 49 | .ow { font-weight: bold } // Operator.Word 50 | .w { color: #bbb } // Text.Whitespace 51 | .mf { color: #099 } // Literal.Number.Float 52 | .mh { color: #099 } // Literal.Number.Hex 53 | .mi { color: #099 } // Literal.Number.Integer 54 | .mo { color: #099 } // Literal.Number.Oct 55 | .sb { color: #d14 } // Literal.String.Backtick 56 | .sc { color: #d14 } // Literal.String.Char 57 | .sd { color: #d14 } // Literal.String.Doc 58 | .s2 { color: #d14 } // Literal.String.Double 59 | .se { color: #d14 } // Literal.String.Escape 60 | .sh { color: #d14 } // Literal.String.Heredoc 61 | .si { color: #d14 } // Literal.String.Interpol 62 | .sx { color: #d14 } // Literal.String.Other 63 | .sr { color: #009926 } // Literal.String.Regex 64 | .s1 { color: #d14 } // Literal.String.Single 65 | .ss { color: #990073 } // Literal.String.Symbol 66 | .bp { color: #999 } // Name.Builtin.Pseudo 67 | .vc { color: #008080 } // Name.Variable.Class 68 | .vg { color: #008080 } // Name.Variable.Global 69 | .vi { color: #008080 } // Name.Variable.Instance 70 | .il { color: #099 } // Literal.Number.Integer.Long 71 | } 72 | -------------------------------------------------------------------------------- /_includes/gpgc_comments.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |

No comments

6 |
7 | 8 |
9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 | You 18 | today 19 |
20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 |
40 |

Comments are closed

41 |
42 | 43 |
44 |
45 | 46 |
47 |
48 | 49 | 50 | 51 | 59 | 60 | 76 | -------------------------------------------------------------------------------- /_posts/2016-06-20-generating-fractal-glider-logo.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "How to generate the Fractal glider logo" 4 | date: 2016-06-20 13:43:26 +0200 5 | categories: fun 6 | --- 7 | This blog's logo is based on the [glider][glider], a pattern that appears in Conway's game of Life. 8 | Consisting only five cells, this pattern is noteworthy for its interesting behavior. The logo displays the image of a glider, in which individual cells are replaced with scaled images of the same glider, creating a [fractal-like][fractal] structure. 9 | 10 | This image is easy to reproduce with a simple script. It requires Bash and [ImageMagick][imagemagick], which are both available for any modern Linux distro. The full script is available [on GitHub][script]. 11 | 12 | First of all, create basic tiles for composing the image: 13 | {% highlight bash %} 14 | convert -size 1x1 xc:none background.0.gif 15 | convert -size 1x1 xc:black glider.0.gif 16 | {% endhighlight %} 17 | These are just 1x1 white and black pixels. 18 | 19 | Create a function that composes these tiles into the glider shape: 20 | {% highlight bash %} 21 | # $1 --- background tile 22 | # $2 --- glider tile 23 | # $3 --- output 24 | function make_glider { 25 | montage $1 $1 $1 $1 \ 26 | $1 $1 $2 $1 \ 27 | $1 $1 $1 $2 \ 28 | $1 $2 $2 $2 \ 29 | -background none -tile 4x4 -geometry +0+0 $3 30 | } 31 | {% endhighlight %} 32 | It calls the [`montage`][montage] command of ImageMagick to compose a 4x4 glider pattern of the tile images supplied as inputs. If you call this function like this: 33 | {% highlight bash %} 34 | make_glider background.0.gif glider.0.gif glider.1.gif 35 | {% endhighlight %} 36 | a new file `glider.1.gif` will be generated with a 4x4 pixels glider pattern. 37 | 38 | The empty top row and left column have been added to preserve visual space between the 'cells' on each nesting level. 39 | 40 | Note that Bash doesn't actually have any internal representations of the images; it handles only _filesystem paths_ to the images and passes them to `montage` command. As a result, if `glider.1.gif` already exists before the function is called, it will be silently overwritten. 41 | 42 | Now, if we invoke `make_glider` using `glider.1.gif` as the input tile, the result will be a 16x16 glider pattern with each 4x4 cell replaced by a 4x4 glider pattern. Apply it several times on the results of the previous iteration to get a structure fith multiple nesting levels. 43 | {% highlight bash %} 44 | for i in {0..3} 45 | do 46 | next=$((i+1)) 47 | make_glider "background.$i.gif" "glider.$i.gif" "glider.$next.gif" 48 | convert "background.$i.gif" -scale 400% "background.$next.gif" 49 | done 50 | {% endhighlight %} 51 | The next line after the call of `make_glider` uses the very generalist `convert` command to [scale][scale] the background tile 4x --- the same amount that the size of pattern tile increases. 52 | 53 | Finally [trim][trim] the excessive white space from the top and left sides and add a 10px [border][border]: 54 | {% highlight bash %} 55 | convert glider.*.gif -trim +repage -bordercolor none -border 10x10 glider.%d.gif 56 | {% endhighlight %} 57 | 58 | Remove the now redundant background tiles: 59 | {% highlight bash %} 60 | rm background.*.gif 61 | {% endhighlight %} 62 | 63 | That's it! Now you have a set of fractal glider logos with sifferent levels of nesting. Change the number of iterations in the `for` loop to get more or fewer levels. 64 | 65 | {: style="text-align: center;"} 66 | ![A fractal glider logo with 4 nesting levels](/images/glider.4.gif) 67 | 68 | 69 | [glider]:https://en.wikipedia.org/wiki/Glider_%28Conway%27s_Life%29 70 | [fractal]:https://en.wikipedia.org/wiki/Fractal 71 | [imagemagick]:http://imagemagick.org/script/index.php 72 | [script]:https://gist.github.com/fractalglider/4f5d0357447bd0acc69ba15e6e6ba6fb 73 | [montage]:http://www.imagemagick.org/Usage/montage 74 | [scale]:http://www.imagemagick.org/Usage/resize/#scale 75 | [trim]:http://www.imagemagick.org/Usage/crop/#trim 76 | [border]:http://www.imagemagick.org/Usage/crop/#border 77 | 78 | -------------------------------------------------------------------------------- /_sass/_base.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Reset some basic elements 3 | */ 4 | body, h1, h2, h3, h4, h5, h6, 5 | p, blockquote, pre, hr, 6 | dl, dd, ol, ul, figure { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | 12 | 13 | /** 14 | * Basic styling 15 | */ 16 | body { 17 | font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family; 18 | color: $text-color; 19 | background-color: $background-color; 20 | -webkit-text-size-adjust: 100%; 21 | -webkit-font-feature-settings: "kern" 1; 22 | -moz-font-feature-settings: "kern" 1; 23 | -o-font-feature-settings: "kern" 1; 24 | font-feature-settings: "kern" 1; 25 | font-kerning: normal; 26 | } 27 | 28 | 29 | 30 | /** 31 | * Set `margin-bottom` to maintain vertical rhythm 32 | */ 33 | h1, h2, h3, h4, h5, h6, 34 | p, blockquote, pre, 35 | ul, ol, dl, figure, 36 | %vertical-rhythm { 37 | margin-bottom: $spacing-unit / 2; 38 | } 39 | 40 | 41 | 42 | /** 43 | * Images 44 | */ 45 | img { 46 | max-width: 100%; 47 | vertical-align: middle; 48 | } 49 | 50 | 51 | 52 | /** 53 | * Figures 54 | */ 55 | figure > img { 56 | display: block; 57 | } 58 | 59 | figcaption { 60 | font-size: $small-font-size; 61 | } 62 | 63 | 64 | 65 | /** 66 | * Lists 67 | */ 68 | ul, ol { 69 | margin-left: $spacing-unit; 70 | } 71 | 72 | li { 73 | > ul, 74 | > ol { 75 | margin-bottom: 0; 76 | } 77 | } 78 | 79 | 80 | 81 | /** 82 | * Headings 83 | */ 84 | h1, h2, h3, h4, h5, h6 { 85 | font-weight: $base-font-weight; 86 | } 87 | 88 | 89 | 90 | /** 91 | * Links 92 | */ 93 | a { 94 | color: $brand-color; 95 | text-decoration: none; 96 | 97 | &:visited { 98 | color: darken($brand-color, 15%); 99 | } 100 | 101 | &:hover { 102 | color: $text-color; 103 | text-decoration: underline; 104 | } 105 | } 106 | 107 | 108 | 109 | /** 110 | * Blockquotes 111 | */ 112 | blockquote { 113 | color: $grey-color; 114 | border-left: 4px solid $grey-color-light; 115 | padding-left: $spacing-unit / 2; 116 | font-size: 18px; 117 | letter-spacing: -1px; 118 | font-style: italic; 119 | 120 | > :last-child { 121 | margin-bottom: 0; 122 | } 123 | } 124 | 125 | 126 | 127 | /** 128 | * Code formatting 129 | */ 130 | pre, 131 | code { 132 | font-size: 15px; 133 | border: 1px solid $grey-color-light; 134 | border-radius: 3px; 135 | background-color: #eef; 136 | } 137 | 138 | code { 139 | padding: 1px 5px; 140 | } 141 | 142 | pre { 143 | padding: 8px 12px; 144 | overflow-x: auto; 145 | 146 | > code { 147 | border: 0; 148 | padding-right: 0; 149 | padding-left: 0; 150 | } 151 | } 152 | 153 | 154 | 155 | /** 156 | * Wrapper 157 | */ 158 | .wrapper { 159 | max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2)); 160 | max-width: calc(#{$content-width} - (#{$spacing-unit} * 2)); 161 | margin-right: auto; 162 | margin-left: auto; 163 | padding-right: $spacing-unit; 164 | padding-left: $spacing-unit; 165 | @extend %clearfix; 166 | 167 | @include media-query($on-laptop) { 168 | max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit})); 169 | max-width: calc(#{$content-width} - (#{$spacing-unit})); 170 | padding-right: $spacing-unit / 2; 171 | padding-left: $spacing-unit / 2; 172 | } 173 | } 174 | 175 | 176 | 177 | /** 178 | * Clearfix 179 | */ 180 | %clearfix { 181 | 182 | &:after { 183 | content: ""; 184 | display: table; 185 | clear: both; 186 | } 187 | } 188 | 189 | 190 | 191 | /** 192 | * Icons 193 | */ 194 | .icon { 195 | 196 | > svg { 197 | display: inline-block; 198 | width: 16px; 199 | height: 16px; 200 | vertical-align: middle; 201 | 202 | path { 203 | fill: $grey-color; 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /_posts/2018-02-13-testing-mouse-scroll-wheel.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Testing the mouse scroll wheel 4 | date: 2018-02-13 13:37:00 +0200 5 | categories: fun 6 | --- 7 | 8 |
9 | 10 | 11 | 12 |
13 | A few days ago I noticed that the [scroll wheel][scroll_wheel] of my mouse stopped working properly. While being rotated in one direction, it sometimes generates events as if it were scrolled in the opposite one. This results in a jerking motion which makes it very inconvenient to browse any lengthy text, not to mention playing games. 14 | 15 | Trying to investigate the issue, I looked for a tool to inspect the scroll events as they are spawned and spot any anomalies. In fact, since I wasn't going to install random apps into my system, I was looking for web pages only. For some reason, I couldn't find one, so I decided to write my own. 16 | 17 | The programming language [Processing][processing] ([Wikipedia][processing_wikipedia]) proved to be the right tool for the job. Its standard library, designed for interactive 2D graphics, allowed me to create a very simple (55 lines long) [program][sketch_github]. 18 | 19 | [scroll_wheel]:https://en.wikipedia.org/wiki/Scroll_wheel 20 | [processing]:https://processing.org 21 | [processing_wikipedia]:https://en.wikipedia.org/wiki/Processing_(programming_language) 22 | [sketch_github]:https://github.com/fractalglider/fractalglider.github.io/blob/master/scripts/mouseScrollTest/mouseScrollTest.pde 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | Try it yourself. Hover your mouse pointer over the canvas and scroll in one direction as fast as you can. The program will draw a colored streamer moving to the right and also in the direction you're scrolling. As long as you don't change scrolling direction, the streamer should stay the same color. If you see spots of a different color, it means that your mouse wheel is glitchy too. 32 | 33 | Having written the program to run in the Processing sketchbook, I decided to share it by publishing it here. I was spared the need to port my code to Javascript, thanks to an awesome library called [Processing.js][processingjs] ([Wikipedia][processingjs_wikipedia]). It is an implementation of Processing in Javascript, which can draw a sketch inside a HTML canvas. So I created a basic HTML page and linked my code along with the library. But not everything worked right away --- hardly a surprise when it comes to web page development. 34 | 35 | It turns out that Processing.js version 1.4.8 does not handle mouse scroll events. I guess this is caused by the sad fact that every browser [handles them in a different way][browser_problems]. Fortunately, Processing.js allows [calling Processing functions from the Javascript code][processing_js_binding]. This means that a custom event handler can solve the issue. 36 | 37 | I came across an [implementation][js_scroll_handler] which is claimed to work in most new browsers. After some [tweaks][event_handling_tweak], I managed to fit it to my needs. The scroll event handler [turned out to be][event_handler_github] more complex than the original program itself, mostly due to quirks of different browsers. Good thing I don't have to program webpages for a living! 38 | 39 | [processingjs]:http://processingjs.org/ 40 | [processingjs_wikipedia]:https://en.wikipedia.org/wiki/Processing.js 41 | [processing_js_binding]:http://processingjs.org/articles/jsQuickStart.html#accessingprocessingfromjs 42 | [browser_problems]:https://stackoverflow.com/questions/25204282/mousewheel-wheel-and-dommousescroll-in-javascript 43 | [js_scroll_handler]:http://www.emanueleferonato.com/2006/07/29/mouse-wheel-handler-in-javascript 44 | [event_handling_tweak]:https://stackoverflow.com/questions/10313142/javascript-capture-mouse-wheel-event-and-do-not-scroll-the-page 45 | [event_handler_github]:https://github.com/fractalglider/fractalglider.github.io/blob/master/scripts/mouseScrollTest/scrollEventHandler.js 46 |
47 | 48 |
49 | -------------------------------------------------------------------------------- /public/css/gpgc_styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Comments for GitHub Pages via Liquid/Jekyll 3 | * 4 | * Copyright 2015 Joe Friedrichsen 5 | * Released under the MIT license. 6 | * 7 | * Learn more at https://github.com/wireddown/ghpages-ghcomments 8 | */ 9 | 10 | /* 11 | * Color theme 12 | * 13 | */ 14 | 15 | .gpgc-color-theme .gpgc-large-secondary-button, 16 | .gpgc-color-theme .gpgc-normal-primary-button, 17 | .gpgc-color-theme .gpgc-tab, 18 | .gpgc-color-theme .gpgc-tab .selected, 19 | .gpgc-color-theme .gpgc-new-comment-form-textarea { 20 | border: solid 1px #90a959; 21 | } 22 | 23 | .gpgc-color-theme .gpgc-large-secondary-button { 24 | color: #90a959; 25 | } 26 | 27 | .gpgc-color-theme .gpgc-normal-primary-button, 28 | .gpgc-color-theme .gpgc-tab { 29 | background-color: #90a959; 30 | } 31 | 32 | /* 33 | * Mix-ins 34 | * 35 | */ 36 | 37 | .gpgc-hidden { 38 | display: none !important; 39 | } 40 | 41 | .gpgc-centered-text { 42 | text-align: center; 43 | } 44 | 45 | .gpgc-comments-font { 46 | font-size: .8rem; 47 | } 48 | 49 | .gpgc-new-section { 50 | margin-top: 3rem !important; 51 | } 52 | 53 | .gpgc-text-button { 54 | padding-left: 1.4rem; 55 | padding-right: 1.4rem; 56 | padding-top: .4rem; 57 | padding-bottom: .4rem; 58 | cursor: pointer; 59 | } 60 | 61 | /* 62 | * Wider display overrides 63 | * 64 | */ 65 | 66 | @media (min-width: 410px) { 67 | .gpgc-normal-primary-button { 68 | float: right; 69 | display: inline; 70 | } 71 | 72 | .gpgc-tabs { 73 | float: right; 74 | padding-left: 0px; 75 | } 76 | 77 | .gpgc-new-comment-actions { 78 | padding: .4rem .4rem 3.2rem !important; 79 | } 80 | } 81 | 82 | /* 83 | * Classes 84 | * 85 | */ 86 | 87 | .gpgc-actions { 88 | overflow: hidden; /* clearfix */ 89 | margin-left: -1rem; 90 | margin-right: -1rem; 91 | } 92 | 93 | .gpgc-action { 94 | display: block; 95 | padding: 1rem; 96 | } 97 | 98 | .gpgc-large-secondary-button { 99 | padding-top: 1rem; 100 | padding-bottom: 1rem; 101 | width: 14rem; 102 | font-family: "PT Sans", Helvetica, Arial, sans-serif; 103 | font-size: 1rem; 104 | background-color: #fff; 105 | cursor: pointer; 106 | } 107 | 108 | .gpgc-large-secondary-button:hover { 109 | background-color: #f5f5f5; 110 | } 111 | 112 | .gpgc_last_div { 113 | padding-top: 12rem; 114 | } 115 | 116 | /* 117 | * Comment classes 118 | * 119 | */ 120 | 121 | .gpgc-comment { 122 | margin-top: .8rem; 123 | background-color: #eee; 124 | border: solid 1px #eee; 125 | border-radius: 0px 0px 24px 24px; 126 | -webkit-border-radius: 0px 0px 24px 24px; 127 | } 128 | 129 | .gpgc-comment-header { 130 | padding-top: .4rem; 131 | padding-right: .4rem; 132 | padding-left: .4rem; 133 | margin-bottom: .2rem; 134 | } 135 | 136 | .gpgc-avatar { 137 | display: inline; 138 | margin-bottom: 0px; 139 | margin-right: .4rem; 140 | vertical-align: baseline; 141 | border-radius: 0px; 142 | } 143 | 144 | .gpgc-comment-contents { 145 | padding: .4rem .4rem 0 .4rem; 146 | } 147 | 148 | /* 149 | * New comment form classes 150 | * 151 | */ 152 | 153 | .gpgc-new-comment { 154 | min-height: 4rem; 155 | } 156 | 157 | .gpgc-new-comment-form { 158 | padding-left: .4rem; 159 | padding-right: .4rem; 160 | } 161 | 162 | .gpgc-tabs { 163 | margin-top: .4rem; 164 | padding-left: 5px; 165 | } 166 | 167 | .gpgc-tab { 168 | margin-left: -5px; 169 | color: #fff; 170 | } 171 | 172 | .gpgc-tab.selected { 173 | background-color: #fff; 174 | color: inherit; 175 | cursor: auto; 176 | } 177 | 178 | .gpgc-new-comment-form-textarea { 179 | font-family: inherit; 180 | display: block; 181 | width: 100%; 182 | min-height: 8rem; 183 | margin-left: auto; 184 | margin-right: auto; 185 | padding: .4rem; 186 | resize: vertical; 187 | } 188 | 189 | .gpgc-new-comment-actions { 190 | padding: 0.4rem 0.4rem 1rem; 191 | } 192 | 193 | .gpgc-normal-primary-button { 194 | } 195 | 196 | .gpgc-normal-primary-button strong { 197 | color: #fff; 198 | } 199 | 200 | button:disabled { 201 | border: solid 1px #888 !important; 202 | background-color: #aaa !important; 203 | cursor: auto !important; 204 | } 205 | 206 | .gpgc-comment-help { 207 | padding: .4rem; 208 | } 209 | 210 | .gpgc-help-message { 211 | } 212 | 213 | .gpgc-help-error { 214 | padding: .4rem; 215 | margin: .4rem; 216 | border: solid 2px #ab4642; 217 | background-color: #ffeded; 218 | } 219 | -------------------------------------------------------------------------------- /_posts/2016-06-27-parallel-runners-junit4.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Parallel runners for Junit 4" 4 | date: 2016-06-27 13:37:00 +0200 5 | categories: java 6 | --- 7 | Sometimes the capability of running [JUnit][junit] tests in parallel is very handy. When you have a lot of tests which don't interfere with each other, you can cut the execution time of the whole suite as many times as many processor cores you have available. Although JUnit itself does not provide support for parallel test execution, there are several ways to achieve this. 8 | 9 | The first solution that springs to mind is using [Maven's][maven] Surefire plugin. It has great support for [running JUnit tests][surefire-junit] either in concurrent threads or even in separate JVM processes. However, it is possible to run JUnit tests in parallel without Maven by subclassing standard runners. 10 | 11 | Starting with JUnit 4.7, the runners that descend from [`ParentRunner`][parentrunner], including [`BlockJUnit4ClassRunner`][blockjUnit4classrunner], [`Suite`][suite] and [`Parameterized`][parameterized], use a [`RunnerScheduler`][runnerscheduler] to schedule their child tests (such as test methods for BlockJUnit4ClassRunner). 12 | 13 | The `RunnerScheduler` interface has two methods: 14 | 15 | * `schedule(Runnable childStatement)` is called by the `ParentRunner` to pass the child test which has to be run (either immediately or sometime later at discretion of the scheduler). It is called once for each child test. 16 | * `finished()` is called to notify the scheduler that all the children have been scheduled and no more will be incoming. It is called only once per parent runner. 17 | 18 | Note that the parent runner will proceed to checking the test results immediately after `finished()` returns, regardless of whether child tests are completed yet. Since the default scheduler runs the child tests synchronously right when they are scheduled, this does not pose a problem. However, if we are to make a parallel scheduler, it's implementation of `finished()` should block until all the child tests are completed. 19 | 20 | Using the custom scheduler is simple --- you just need to inject it into the parent runner by calling its method [`setScheduler(RunnerScheduler)`][setscheduler]. Because JUnit creaties runner instances using reflection, you'll still need to define a custom subclass of your runner and override its constructor. The following example shows how to do it for `BlockJUnit4ClassRunner`, although it can be done the same way for any runner that extends `ParentRunner`, including `Parameterized` and `Suite`. 21 | 22 | {% highlight java %} 23 | class BlockJUnit4ClassRunner extends org.junit.runners.BlockJUnit4ClassRunner { 24 | public BlockJUnit4ClassRunner(Class klass) 25 | throws InitializationError { 26 | super(klass); 27 | this.setScheduler(new ParallelRunnerScheduler()); 28 | } 29 | } 30 | {% endhighlight %} 31 | 32 | Note that any objects that are shared between individual child tests (e. g. those defined by [`@ClassRule`][classrule]) *must be thread safe.* 33 | 34 | An example project with an implementation of the parallel scheduler and an example of its usage is available [on GitHub][example-github]. 35 | 36 | Both test suites and individual test cases can still be launched by any application that can launch JUnit tests (e. g. Eclipse or IntelliJ IDEA). Unfortunately, both Eclipse or IntelliJ IDEA display the test results erroneously. Eclipse assigns _all_ failure causes to the first test that failed, showing the rest of failed tests as successful; IntelliJ IDEA may display duplicate entries for test cases and test suites. Regardless, in both cases the tests itself are executed correctly and each test failure can be identified. Maven displays the results correctly, but if Maven is available, you may be better off parallellizing test with the Surefire plugin. 37 | 38 | [junit]:http://junit.org/junit4/ 39 | [maven]:https://maven.apache.org/ 40 | [surefire-junit]:https://maven.apache.org/surefire/maven-surefire-plugin/examples/fork-options-and-parallel-execution.html 41 | [parentrunner]:http://junit.org/junit4/javadoc/latest/org/junit/runners/ParentRunner.html 42 | [blockjUnit4classrunner]:http://junit.org/junit4/javadoc/latest/org/junit/runners/BlockJUnit4ClassRunner.html 43 | [suite]:http://junit.org/junit4/javadoc/latest/org/junit/runners/Suite.html 44 | [parameterized]:http://junit.org/junit4/javadoc/latest/org/junit/runners/Parameterized.html 45 | [runnerscheduler]:http://junit.org/junit4/javadoc/latest/org/junit/runners/model/RunnerScheduler.html 46 | [setscheduler]:http://junit.org/junit4/javadoc/latest/org/junit/runners/ParentRunner.html#setScheduler(org.junit.runners.model.RunnerScheduler) 47 | [example-github]:https://github.com/odisseus/junit-parallel-runners 48 | [classrule]:http://junit.org/junit4/javadoc/latest/org/junit/ClassRule.html 49 | 50 | -------------------------------------------------------------------------------- /_sass/_layout.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Site header 3 | */ 4 | .site-header { 5 | border-top: 5px solid $grey-color-dark; 6 | border-bottom: 1px solid $grey-color-light; 7 | min-height: 56px; 8 | 9 | // Positioning context for the mobile navigation icon 10 | position: relative; 11 | } 12 | 13 | .site-title { 14 | font-size: 72px; 15 | font-weight: 100; 16 | font-family: "Hypertext 4D", $base-font-family; 17 | line-height: 56px; 18 | letter-spacing: -1px; 19 | margin-bottom: 0; 20 | float: left; 21 | 22 | &, 23 | &:visited { 24 | color: $grey-color-dark; 25 | } 26 | } 27 | 28 | .site-nav { 29 | float: right; 30 | line-height: 56px; 31 | 32 | .menu-icon { 33 | display: none; 34 | } 35 | 36 | .page-link { 37 | color: $text-color; 38 | line-height: $base-line-height; 39 | 40 | // Gaps between nav items, but not on the last one 41 | &:not(:last-child) { 42 | margin-right: 20px; 43 | } 44 | } 45 | 46 | @include media-query($on-palm) { 47 | position: absolute; 48 | top: 9px; 49 | right: $spacing-unit / 2; 50 | background-color: $background-color; 51 | border: 1px solid $grey-color-light; 52 | border-radius: 5px; 53 | text-align: right; 54 | 55 | .menu-icon { 56 | display: block; 57 | float: right; 58 | width: 36px; 59 | height: 26px; 60 | line-height: 0; 61 | padding-top: 10px; 62 | text-align: center; 63 | 64 | > svg { 65 | width: 18px; 66 | height: 15px; 67 | 68 | path { 69 | fill: $grey-color-dark; 70 | } 71 | } 72 | } 73 | 74 | .trigger { 75 | clear: both; 76 | display: none; 77 | } 78 | 79 | &:hover .trigger { 80 | display: block; 81 | padding-bottom: 5px; 82 | } 83 | 84 | .page-link { 85 | display: block; 86 | padding: 5px 10px; 87 | 88 | &:not(:last-child) { 89 | margin-right: 0; 90 | } 91 | margin-left: 20px; 92 | } 93 | } 94 | } 95 | 96 | 97 | 98 | /** 99 | * Site footer 100 | */ 101 | .site-footer { 102 | border-top: 1px solid $grey-color-light; 103 | padding: $spacing-unit 0; 104 | } 105 | 106 | .footer-heading { 107 | font-size: 18px; 108 | margin-bottom: $spacing-unit / 2; 109 | } 110 | 111 | .contact-list, 112 | .social-media-list { 113 | list-style: none; 114 | margin-left: 0; 115 | } 116 | 117 | .footer-col-wrapper { 118 | font-size: 15px; 119 | color: $grey-color; 120 | margin-left: -$spacing-unit / 2; 121 | @extend %clearfix; 122 | } 123 | 124 | .footer-col { 125 | float: left; 126 | margin-bottom: $spacing-unit / 2; 127 | padding-left: $spacing-unit / 2; 128 | } 129 | 130 | .footer-col-1 { 131 | width: -webkit-calc(35% - (#{$spacing-unit} / 2)); 132 | width: calc(35% - (#{$spacing-unit} / 2)); 133 | } 134 | 135 | .footer-col-2 { 136 | width: -webkit-calc(20% - (#{$spacing-unit} / 2)); 137 | width: calc(20% - (#{$spacing-unit} / 2)); 138 | } 139 | 140 | .footer-col-3 { 141 | width: -webkit-calc(45% - (#{$spacing-unit} / 2)); 142 | width: calc(45% - (#{$spacing-unit} / 2)); 143 | } 144 | 145 | @include media-query($on-laptop) { 146 | .footer-col-1, 147 | .footer-col-2 { 148 | width: -webkit-calc(50% - (#{$spacing-unit} / 2)); 149 | width: calc(50% - (#{$spacing-unit} / 2)); 150 | } 151 | 152 | .footer-col-3 { 153 | width: -webkit-calc(100% - (#{$spacing-unit} / 2)); 154 | width: calc(100% - (#{$spacing-unit} / 2)); 155 | } 156 | } 157 | 158 | @include media-query($on-palm) { 159 | .footer-col { 160 | float: none; 161 | width: -webkit-calc(100% - (#{$spacing-unit} / 2)); 162 | width: calc(100% - (#{$spacing-unit} / 2)); 163 | } 164 | } 165 | 166 | 167 | 168 | /** 169 | * Page content 170 | */ 171 | .page-content { 172 | padding: $spacing-unit 0; 173 | } 174 | 175 | .page-heading { 176 | font-size: 20px; 177 | } 178 | 179 | .post-list { 180 | margin-left: 0; 181 | list-style: none; 182 | 183 | > li { 184 | margin-bottom: $spacing-unit; 185 | } 186 | } 187 | 188 | .post-meta { 189 | font-size: $small-font-size; 190 | color: $grey-color; 191 | } 192 | 193 | .post-link { 194 | display: block; 195 | font-size: 24px; 196 | } 197 | 198 | 199 | 200 | /** 201 | * Posts 202 | */ 203 | .post-header { 204 | margin-bottom: $spacing-unit; 205 | } 206 | 207 | .post-title { 208 | font-size: 42px; 209 | letter-spacing: -1px; 210 | line-height: 1; 211 | 212 | @include media-query($on-laptop) { 213 | font-size: 36px; 214 | } 215 | } 216 | 217 | .post-content { 218 | margin-bottom: $spacing-unit; 219 | 220 | h2 { 221 | font-size: 32px; 222 | 223 | @include media-query($on-laptop) { 224 | font-size: 28px; 225 | } 226 | } 227 | 228 | h3 { 229 | font-size: 26px; 230 | 231 | @include media-query($on-laptop) { 232 | font-size: 22px; 233 | } 234 | } 235 | 236 | h4 { 237 | font-size: 20px; 238 | 239 | @include media-query($on-laptop) { 240 | font-size: 18px; 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /_tools/gpgcCreateCommentIssue.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # gpgcCreateCommentIssue.sh 4 | # Copyright 2015 Joe Friedrichsen 5 | # Licensed under the Apache 2.0 License 6 | 7 | function PrintUsage 8 | { 9 | echo >&2 "Usage:" 10 | echo >&2 " gpgcCreateCommentIssue.sh install 'personal_access_token'" 11 | echo >&2 " gpgcCreateCommentIssue.sh bootstrap" 12 | echo >&2 " gpgcCreateCommentIssue.sh commit" 13 | echo >&2 " gpgcCreateCommentIssue.sh push 'personal_access_token'" 14 | echo >&2 15 | } 16 | 17 | function Diagnose() 18 | { 19 | local sender="$1" 20 | local message="$2" 21 | if ${Verbose}; then 22 | echo "${sender}: ${message}" >> /dev/stderr 23 | fi 24 | } 25 | 26 | function InstallPreCommit() 27 | { 28 | local preCommitScript="${GitRepoRoot}/.git/hooks/pre-commit" 29 | local installTag="hook from wireddown/ghpages-ghcomments" 30 | local beginInstallTag="### BEGIN ${installTag}" 31 | local endInstallTag="### END ${installTag}" 32 | Diagnose "InstallPreCommit" "preCommitScript == ${preCommitScript}" 33 | 34 | if ! test -f "${preCommitScript}"; then 35 | Diagnose "InstallPreCommit" "Creating ${preCommitScript}" 36 | echo "#!/bin/bash" > "${preCommitScript}" 37 | else 38 | if $(grep -q "${installTag}" "${preCommitScript}" 2>/dev/null); then 39 | Diagnose "InstallPreCommit" "Updating ${preCommitScript}" 40 | local everythingExceptThisHook="$(cat "${preCommitScript}" | sed "\!${beginInstallTag}!,\!${endInstallTag}!d")" 41 | echo "${everythingExceptThisHook}" > "${preCommitScript}" 42 | fi 43 | fi 44 | 45 | cat >> "${preCommitScript}" <<-pre-commit-hook 46 | 47 | ${beginInstallTag} 48 | 49 | gpgcCreateCommentIssue="${GitRepoRoot}/_tools/gpgcCreateCommentIssue.sh" 50 | 51 | if test -x "\${gpgcCreateCommentIssue}"; then 52 | "\${gpgcCreateCommentIssue}" commit 53 | fi 54 | 55 | ${endInstallTag} 56 | 57 | pre-commit-hook 58 | } 59 | 60 | function InstallPrePush() 61 | { 62 | local prePushScript="${GitRepoRoot}/.git/hooks/pre-push" 63 | local installTag="hook from wireddown/ghpages-ghcomments" 64 | local beginInstallTag="### BEGIN ${installTag}" 65 | local endInstallTag="### END ${installTag}" 66 | Diagnose "InstallPrePush" "prePushScript == ${prePushScript}" 67 | 68 | if ! test -f "${prePushScript}"; then 69 | Diagnose "InstallPrePush" "Creating ${prePushScript}" 70 | echo "#!/bin/bash" > "${prePushScript}" 71 | else 72 | if $(grep -q "${installTag}" "${prePushScript}" 2>/dev/null); then 73 | Diagnose "InstallPrePush" "Updating ${prePushScript}" 74 | local everythingExceptThisHook="$(cat "${prePushScript}" | sed "\!${beginInstallTag}!,\!${endInstallTag}!d")" 75 | echo "${everythingExceptThisHook}" > "${prePushScript}" 76 | fi 77 | fi 78 | 79 | cat >> "${prePushScript}" <<-pre-push-hook 80 | 81 | ${beginInstallTag} 82 | 83 | gpgcCreateCommentIssue="${GitRepoRoot}/_tools/gpgcCreateCommentIssue.sh" 84 | 85 | if test -x "\${gpgcCreateCommentIssue}"; then 86 | "\${gpgcCreateCommentIssue}" push ${PersonalAccessToken} 87 | fi 88 | 89 | ${endInstallTag} 90 | 91 | pre-push-hook 92 | } 93 | 94 | function Install 95 | { 96 | InstallPreCommit 97 | InstallPrePush 98 | } 99 | 100 | function Bootstrap 101 | { 102 | local allPosts="$(find _posts -type f)" 103 | Diagnose "Bootstrap" "allPosts == ${allPosts}" 104 | AddPostsToCache ${allPosts} 105 | } 106 | 107 | function Commit 108 | { 109 | local changedPosts="$(git diff --name-status --cached | grep "_posts/")" 110 | local allChangedPosts="$(echo "${changedPosts}" | awk '{print $2}')" 111 | Diagnose "Commit" "changedPosts == ${changedPosts}" 112 | Diagnose "Commit" "allChangedPosts == ${allChangedPosts}" 113 | AddPostsToCache ${allChangedPosts} 114 | } 115 | 116 | function AddPostsToCache() 117 | { 118 | if test -n "$*"; then 119 | for post in $@; do 120 | if ! $(grep -q "${post}" "${GpgcCacheFile}" 2>/dev/null); then 121 | Diagnose "AddPostsToCache" "Adding ${post} to ${GpgcCacheFile}" 122 | echo "${post}" >> "${GpgcCacheFile}" 123 | fi 124 | done 125 | fi 126 | } 127 | 128 | function GetValueFromYml() 129 | { 130 | local ymlFile="$1" 131 | local ymlKey="$2" 132 | local value="$(grep "^${ymlKey}:" "${ymlFile}" 2>/dev/null | head -n1 | sed "s/${ymlKey}:[[:space:]]//g" | sed s/[[:space:]]*#.*$//g | tr -d '"')" 133 | Diagnose "GetValueFromYml" "ymlFile == ${ymlFile}" 134 | Diagnose "GetValueFromYml" "ymlKey == ${ymlKey}" 135 | Diagnose "GetValueFromYml" "value == ${value}" 136 | echo "${value}" 137 | } 138 | 139 | function Push 140 | { 141 | local siteDataFile="${GitRepoRoot}/_config.yml" 142 | Diagnose "Push" "siteDataFile == ${siteDataFile}" 143 | 144 | local repo_owner="$(GetValueFromYml "${GpgcDataFile}" repo_owner)" 145 | local repo_name="$(GetValueFromYml "${GpgcDataFile}" repo_name)" 146 | local label_name="$(GetValueFromYml "${GpgcDataFile}" label_name)" 147 | local label_color="$(GetValueFromYml "${GpgcDataFile}" label_color)" 148 | 149 | if ! $(LabelExists "${repo_owner}" "${repo_name}" "${label_name}"); then 150 | if $(CreateLabel "${repo_owner}" "${repo_name}" "${label_name}" "${label_color}"); then 151 | echo "Created label \"${label_name}\" for \"${repo_owner}/${repo_name}\"" 152 | else 153 | echo "Error: could not create label \"${label_name}\" for \"${repo_owner}/${repo_name}\"" 154 | exit 155 | fi 156 | fi 157 | 158 | local allCommittedPosts="$(cat "${GpgcCacheFile}" 2>/dev/null)" 159 | Diagnose "Push" "allCommittedPosts == ${allCommittedPosts}" 160 | local resetCacheFile=false 161 | if test -n "${allCommittedPosts}"; then 162 | for committed_post in ${allCommittedPosts}; do 163 | if test -f "${committed_post}"; then 164 | local post_title="$(GetValueFromYml "${committed_post}" title)" 165 | local site="$(GetValueFromYml "${siteDataFile}" url)" 166 | local year="$(basename "${committed_post}" | awk -F '-' '{print $1}')" 167 | local month="$(basename "${committed_post}" | awk -F '-' '{print $2}')" 168 | local day="$(basename "${committed_post}" | awk -F '-' '{print $3}')" 169 | local post_slug="$(basename "${committed_post}" | sed 's/^[0-9]\+-[0-9]\+-[0-9]\+-//' | sed 's/\.[a-zA-Z]\+$//')" 170 | local post_url="${site}/${year}/${month}/${day}/${post_slug}" 171 | 172 | if ! $(IssueExists "${repo_owner}" "${repo_name}" "${post_title}"); then 173 | if $(CreateIssue "${repo_owner}" "${repo_name}" "${post_title}" "${post_url}" "${label_name}"); then 174 | echo "Created issue \"${post_title}\" for \"${repo_owner}/${repo_name}\"" 175 | resetCacheFile=: 176 | else 177 | echo "Error: could not create issue \"${post_title}\" for \"${repo_owner}/${repo_name}\"" 178 | exit 179 | fi 180 | else 181 | resetCacheFile=: 182 | fi 183 | 184 | if ${resetCacheFile}; then 185 | Diagnose "Push" "Resetting ${GpgcCacheFile}" 186 | echo > "${GpgcCacheFile}" 187 | fi 188 | fi 189 | done 190 | fi 191 | 192 | } 193 | 194 | function LabelExists() 195 | { 196 | local owner="$1" 197 | local repo="$2" 198 | local label="$3" 199 | Diagnose "LabelExists" "Querying \"https://api.github.com/repos/${owner}/${repo}/labels\" for \"${label}\"" 200 | local labelList="$(curl -s https://api.github.com/repos/${owner}/${repo}/labels)" 201 | Diagnose "LabelExists" "${labelList}" 202 | if echo "${labelList}" | grep -q "\"name\": \"${label}\""; then 203 | echo : 204 | else 205 | echo false 206 | fi 207 | } 208 | 209 | function CreateLabel() 210 | { 211 | local owner="$1" 212 | local repo="$2" 213 | local label="$3" 214 | local color="$4" 215 | local body="{\"name\":\"${label}\",\"color\":\"${color}\"}" 216 | Diagnose "CreateLabel" "Posting to \"https://api.github.com/repos/${owner}/${repo}/labels\" with '${body}'" 217 | local creationResponse="$(curl -s -H "${AuthHeader}" -d "${body}" https://api.github.com/repos/${owner}/${repo}/labels)" 218 | if echo "${creationResponse}" | grep -q "\"name\": \"${label}\""; then 219 | echo : 220 | else 221 | echo false 222 | echo "${creationResponse}" > /dev/stderr 223 | if ! ${Verbose} ; then echo "For more information, set 'enable_diagnostics' to 'true' in ${GpgcDataFile}" > /dev/stderr; fi 224 | fi 225 | } 226 | 227 | function RawUrlEncode() 228 | { 229 | local string="${1}" 230 | local strlen=${#string} 231 | local encoded="" 232 | Diagnose "RawUrlEncode" "Encoding \"${string}\"" 233 | 234 | for (( pos=0 ; pos /dev/stderr 277 | if ! ${Verbose} ; then echo "For more information, set 'enable_diagnostics' to 'true' in ${GpgcDataFile}" > /dev/stderr; fi 278 | fi 279 | } 280 | 281 | PersonalAccessToken="$2" 282 | AuthHeader="Authorization: token ${PersonalAccessToken}" 283 | GitRepoRoot="$(git rev-parse --show-toplevel)" 284 | GpgcCacheFile="${GitRepoRoot}/.git/gpgc_cache" 285 | GpgcDataFile="${GitRepoRoot}/_data/gpgc.yml" 286 | 287 | Verbose=false 288 | enable_diagnostics="$(GetValueFromYml "${GpgcDataFile}" enable_diagnostics)" 289 | if test "x${enable_diagnostics}" = "xtrue"; then Verbose=: ; fi 290 | Diagnose "(global)" "enable_diagnostics == ${enable_diagnostics}" 291 | Diagnose "(global)" "GitRepoRoot == ${GitRepoRoot}" 292 | Diagnose "(global)" "GpgcCacheFile == ${GpgcCacheFile}" 293 | Diagnose "(global)" "GpgcDataFile == ${GpgcDataFile}" 294 | 295 | Diagnose "(global)" "operation == $1" 296 | case "$1" in 297 | install) Install ;; 298 | bootstrap) Bootstrap ;; 299 | commit) Commit ;; 300 | push) Push ;; 301 | *) PrintUsage; echo "Unknown action \"$1\""; exit 1 ;; 302 | esac 303 | -------------------------------------------------------------------------------- /public/js/gpgc_core.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Comments for GitHub Pages via Liquid/Jekyll 3 | * 4 | * Copyright 2015 Joe Friedrichsen 5 | * Released under the Apache 2.0 license. 6 | * 7 | * Learn more at https://github.com/wireddown/ghpages-ghcomments 8 | * 9 | */ 10 | 11 | /* Globals */ 12 | 13 | var ShortMonthForIndex = { 0: "Jan", 1: "Feb", 2: "Mar", 3: "Apr", 4: "May", 5: "Jun", 6: "Jul", 7: "Aug", 8: "Sep", 9: "Oct", 10: "Nov", 11: "Dec" }; 14 | var AccessToken = ""; 15 | var IssueUrl = ""; 16 | var CommentsUrl = ""; 17 | var CommentsArray = []; 18 | var StateChallenge = ""; 19 | var WriteMode = "write", PreviewMode = "preview"; 20 | 21 | var AllCommentsDiv = document.getElementById("gpgc_all_comments"); 22 | var NoCommentsDiv = document.getElementById("gpgc_no_comments"); 23 | var ActionsDiv = document.getElementById("gpgc_actions"); 24 | var ShowCommentsButton = document.getElementById("show_comments_button"); 25 | 26 | var DisabledCommentsDiv = document.getElementById("gpgc_disabled"); 27 | var NewCommentDiv = document.getElementById("gpgc_new_comment"); 28 | var WriteButton = document.getElementById("write_button"); 29 | var WriteDiv = document.getElementById("write_div"); 30 | var CommentMarkdown = document.getElementById("new_comment_field"); 31 | var PreviewButton = document.getElementById("preview_button"); 32 | var PreviewDiv = document.getElementById("preview_div"); 33 | 34 | var ReaderGitHubUrl = document.getElementById("gpgc_reader_url"); 35 | var ReaderAvatarUrl = document.getElementById("gpgc_reader_avatar"); 36 | var ReaderLogin = document.getElementById("gpgc_reader_login"); 37 | var LoginButton = document.getElementById("login_button"); 38 | var SubmitButton = document.getElementById("submit_button"); 39 | 40 | var HelpMessageDiv = document.getElementById("help_message"); 41 | var ErrorDiv = document.getElementById("gpgc_reader_error"); 42 | 43 | /* main */ 44 | 45 | function gpgc_main() { 46 | if (gpgc.enable_diagnostics) 47 | { 48 | verifyInitialConditions(); 49 | verifyCss(); 50 | } 51 | 52 | if (gpgc.new_comments_disabled) { 53 | disableNewCommentForm(); 54 | } else { 55 | initializeData(); 56 | initializeEvents(); 57 | initializeNewCommentForm(); 58 | } 59 | 60 | findAndCollectComments(gpgc.repo_id, gpgc.issue_title); 61 | } 62 | 63 | /* Data */ 64 | 65 | function initializeData() { 66 | retrieveToken(); 67 | } 68 | 69 | function retrieveToken() { 70 | AccessToken = sessionStorage.getItem("AccessToken"); 71 | if (AccessToken === null) { 72 | AccessToken = ""; 73 | } 74 | } 75 | 76 | function persistToken() { 77 | sessionStorage.setItem("AccessToken", AccessToken); 78 | } 79 | 80 | function clearToken() { 81 | AccessToken = ""; 82 | authenticateUser(); 83 | persistToken(); 84 | } 85 | 86 | /* Events */ 87 | 88 | function initializeEvents() { 89 | window.addEventListener("message", onMessage); 90 | } 91 | 92 | function onMessage(event) { 93 | if (event.data.type === undefined) { return; } 94 | 95 | switch (event.data.type) { 96 | case "login": 97 | if (event.origin === gpgc.site_url && event.data.state === StateChallenge) { 98 | handleLogin(event.data.code); 99 | } 100 | return; 101 | default: 102 | showFatalError("Unknown event: " + JSON.stringify(event.data)); 103 | return; 104 | } 105 | } 106 | 107 | /* New comment form */ 108 | 109 | function disableNewCommentForm() { 110 | hideElement(NewCommentDiv); 111 | showElement(DisabledCommentsDiv); 112 | } 113 | 114 | function initializeNewCommentForm() { 115 | authenticateUser(); 116 | updateCommentFormMode(WriteMode, /* reset: */ false); 117 | } 118 | 119 | function updateCommentFormMode(newMode, reset) { 120 | var elementsToShow = []; 121 | var elementsToHide = []; 122 | 123 | if (newMode === PreviewMode) { 124 | WriteButton.onclick = function () { updateCommentFormMode(WriteMode, /* reset: */ false); }; 125 | WriteButton.classList.remove("selected"); 126 | PreviewButton.onclick = null; 127 | PreviewButton.classList.add("selected"); 128 | elementsToHide.push(WriteDiv); 129 | elementsToShow.push(PreviewDiv); 130 | PreviewDiv.innerHTML = ""; 131 | renderMarkdown(CommentMarkdown.value); 132 | } else if (newMode === WriteMode) { 133 | WriteButton.onclick = null; 134 | WriteButton.classList.add("selected"); 135 | PreviewButton.onclick = function () { updateCommentFormMode(PreviewMode, /* reset: */ false); }; 136 | PreviewButton.classList.remove("selected"); 137 | elementsToShow.push(WriteDiv); 138 | elementsToHide.push(PreviewDiv); 139 | } 140 | 141 | updateElements(elementsToShow, elementsToHide, /* elementsToEnable: */ null, /* elementsToDisable: */ null); 142 | 143 | if (reset) { 144 | CommentMarkdown.value = ""; 145 | PreviewDiv.innerHTML = ""; 146 | } 147 | } 148 | 149 | function updateCommenterInformation(userJson) { 150 | ReaderGitHubUrl.href = userJson.html_url; 151 | ReaderAvatarUrl.src = userJson.avatar_url; 152 | ReaderLogin.innerHTML = userJson.login; 153 | } 154 | 155 | /* GitHub: User authentication */ 156 | 157 | function authenticateUser() { 158 | if (AccessToken.length === 40) { 159 | var userIdUrl = "https://api.github.com/user"; 160 | getGitHubApiRequestWithCompletion( 161 | userIdUrl, 162 | /* data: */ null, 163 | AccessToken, 164 | /* onPreRequest: */ noop, 165 | onUserAuthenticated, 166 | onUserAuthenticationError 167 | ); 168 | } else if (AccessToken.length === 0) { 169 | onAuthenticateUserFailed(); 170 | showCommentHelpMessage("To leave a comment, please login to GitHub."); 171 | } else { 172 | onAuthenticateUserFailed(); 173 | showFatalError("An OAuth token must be 40 characters long, this one is " + AccessToken.length + " long."); 174 | } 175 | } 176 | 177 | function onUserAuthenticated(checkAuthenticationRequest) { 178 | var elementsToShow = [ SubmitButton ]; 179 | var elementsToHide = [ LoginButton ]; 180 | var elementsToEnable = [ SubmitButton ]; 181 | var elementsToDisable = [ ]; 182 | persistToken(); 183 | updateCommenterInformation(JSON.parse(checkAuthenticationRequest.responseText)); 184 | clearCommentHelp(); 185 | updateElements(elementsToShow, elementsToHide, elementsToEnable, elementsToDisable); 186 | } 187 | 188 | function onUserAuthenticationError(checkAuthenticationRequest) { 189 | AccessToken = ""; 190 | var helpErrorMessage = "Sorry, it looks like your login failed. Please try again, or reset your ghpages-ghcomments authorization."; 191 | var isRawHtml = false; 192 | if (gpgc.enable_diagnostics) { 193 | helpErrorMessage = "

gpgc Error: Authentication Failed

Could not authenticate OAuth token

GitHub response:

" + checkAuthenticationRequest.responseText + "

"; 194 | isRawHtml = true; 195 | } 196 | 197 | showCommentHelpError(helpErrorMessage, isRawHtml); 198 | onAuthenticateUserFailed(); 199 | return; 200 | } 201 | 202 | function onAuthenticateUserFailed() { 203 | var elementsToShow = [ LoginButton ]; 204 | var elementsToHide = [ SubmitButton ]; 205 | var elementsToEnable = [ LoginButton ]; 206 | var elementsToDisable = [ SubmitButton ]; 207 | updateElements(elementsToShow, elementsToHide, elementsToEnable, elementsToDisable); 208 | updateCommenterInformation({ 209 | login: "You", 210 | html_url: "https://github.com/wireddown/ghpages-ghcomments", 211 | avatar_url: "https://raw.githubusercontent.com/wireddown/ghpages-ghcomments/gh-pages/public/apple-touch-icon-precomposed.png" 212 | }); 213 | } 214 | 215 | /* GitHub: Web app login */ 216 | 217 | function loginToGitHub() { 218 | var challengeArray = new Uint32Array(2); 219 | window.crypto.getRandomValues(challengeArray); 220 | StateChallenge = challengeArray[0].toString() + challengeArray[1].toString(); 221 | var data = { 222 | "client_id": gpgc.github_application_client_id, 223 | "scope": "public_repo", 224 | "state": StateChallenge, 225 | "redirect_uri": gpgc.github_application_login_redirect_url 226 | }; 227 | 228 | var urlParameters = Object.keys(data).map(function (key) { 229 | return encodeURIComponent(key) + "=" + encodeURIComponent(data[key]); 230 | }).join("&"); 231 | 232 | window.open( 233 | "https://github.com/login/oauth/authorize?" + urlParameters, 234 | "Log In to GitHub", 235 | "resizable,scrollbars,status,width=1024,height=620" 236 | ); 237 | } 238 | 239 | function handleLogin(code) { 240 | disableElement(LoginButton); 241 | clearCommentHelp(); 242 | showCommentHelpMessage("Finishing login..."); 243 | getTokenUsingCode(code); 244 | } 245 | 246 | function getTokenUsingCode(code) { 247 | getGitHubApiRequestWithCompletion( 248 | gpgc.github_application_code_authenticator_url + code, 249 | /* data: */ null, 250 | /* accessToken: */ null, 251 | /* onPreRequest: */ noop, 252 | onTokenRetrieved, 253 | onRetrieveTokenFailed 254 | ); 255 | } 256 | 257 | function onTokenRetrieved(retrieveTokenRequest) { 258 | var tokenResponse = JSON.parse(retrieveTokenRequest.responseText); 259 | if (tokenResponse.token !== undefined) { 260 | AccessToken = tokenResponse.token; 261 | persistToken(); 262 | } else { 263 | onRetrieveTokenFailed(retrieveTokenRequest); 264 | } 265 | 266 | authenticateUser(); 267 | } 268 | 269 | function onRetrieveTokenFailed(retrieveTokenRequest) { 270 | enableElement(LoginButton); 271 | clearCommentHelp(); 272 | showFatalError("onRetrieveTokenFailed: \n\n" + retrieveTokenRequest.responseText); 273 | } 274 | 275 | /* GitHub: Search for comment issue */ 276 | 277 | function findAndCollectComments(repositoryID, issueTitle) { 278 | var safeQuery = encodeURI(issueTitle); 279 | var seachQueryUrl = "https://api.github.com/search/issues?q=" + safeQuery + "+repo:" + repositoryID + "+type:issue+in:title"; 280 | getGitHubApiRequestWithCompletion( 281 | seachQueryUrl, 282 | /* data: */ null, 283 | AccessToken, 284 | /* onPreRequest: */ noop, 285 | onSearchComplete, 286 | onSearchError 287 | ); 288 | } 289 | 290 | function onSearchComplete(searchRequest) { 291 | var searchResults = JSON.parse(searchRequest.responseText); 292 | if (searchResults.total_count === 1) { 293 | var shouldQueryComments = !isIssueMuted(searchResults.items[0].labels); 294 | if (shouldQueryComments) { 295 | IssueUrl = searchResults.items[0].html_url; 296 | CommentsUrl = searchResults.items[0].comments_url; 297 | insertIssueLink(IssueUrl); 298 | getGitHubApiRequestWithCompletion( 299 | CommentsUrl, 300 | /* data: */ null, 301 | AccessToken, 302 | /* onPreRequest: */ noop, 303 | onQueryComments, 304 | onQueryCommentsError 305 | ); 306 | } else { 307 | disableNewCommentForm(); 308 | } 309 | } else { 310 | onSearchError(searchRequest); 311 | } 312 | } 313 | 314 | function insertIssueLink(issueUrl) { 315 | var issueLinkMessage = "

Comments to this post can also be created and viewed on GitHub.

"; 316 | document.getElementById("gpgc_issue_link").innerHTML = issueLinkMessage; 317 | } 318 | 319 | function onSearchError(searchRequest) { 320 | if (gpgc.enable_diagnostics) { 321 | var searchErrorMessage = ""; 322 | if (searchRequest.status !== 200) { 323 | searchErrorMessage = "

gpgc Error: Search Failed

Could not search GitHub repository " + gpgc.repo_id + ".

GitHub response:

" + searchRequest.responseText + "

Check:

  • repo_owner in _data/gpgc.yml for typos.
  • repo_name in _data/gpgc.yml for typos.

"; 324 | } 325 | 326 | var missingIssueMessage = ""; 327 | var searchResults = JSON.parse(searchRequest.responseText); 328 | if (searchResults.total_count !== undefined && searchResults.total_count === 0) { 329 | missingIssueMessage = "

gpgc Error: Missing Issue

Could not find comment issue with the title " + gpgc.issue_title + " in the repository " + gpgc.repo_id + ".

Check:

  • for typos in the Jekyll title front matter for this post: " + gpgc.page_path + ".
  • that the repo_name in _data/gpgc.yml matches the repository for this site.
  • the terminal output from git push for other error messages if the git hooks are installed.

"; 330 | } 331 | 332 | var allMessagesHtml = searchErrorMessage + missingIssueMessage; 333 | if (allMessagesHtml.length > 0) { 334 | allMessagesHtml += "

Search Help

Verify your site's configuration with the setup instructions and refer to the verbose usage for step-by-step details.

Contact ghpages-ghcomments for more help.

"; 335 | 336 | ErrorDiv.innerHTML += allMessagesHtml; 337 | showElement(ErrorDiv); 338 | } 339 | } else { 340 | if (searchRequest.status === 401) { 341 | AccessToken = ""; 342 | findAndCollectComments(gpgc.repo_id, gpgc.issue_title); 343 | } else { 344 | showFatalError("onSearchError: \n\n" + searchRequest.responseText); 345 | } 346 | } 347 | } 348 | 349 | function isIssueMuted(labelsArray) { 350 | for (var i = 0; i < labelsArray.length; i++) { 351 | if (labelsArray[i].name === "GPGC Muted") { 352 | return true; 353 | } 354 | } 355 | 356 | return false; 357 | } 358 | 359 | /* GitHub: Retrieve comments */ 360 | 361 | function onQueryComments(commentRequest) { 362 | CommentsArray = CommentsArray.concat(JSON.parse(commentRequest.responseText)); 363 | var commentsPages = commentRequest.getResponseHeader("Link"); 364 | if (commentsPages) { 365 | var commentsLinks = commentsPages.split(","); 366 | for (var i = 0; i < commentsLinks.length; i++) { 367 | if (commentsLinks[i].search('rel="next"') > 0) { 368 | var linkStart = commentsLinks[i].search("<"); 369 | var linkStop = commentsLinks[i].search(">"); 370 | var nextLink = commentsLinks[i].substring(linkStart + 1, linkStop); 371 | getGitHubApiRequestWithCompletion( 372 | nextLink, 373 | /* data: */ null, 374 | AccessToken, 375 | /* onPreRequest: */ noop, 376 | onQueryComments, 377 | onQueryCommentsError); 378 | return; 379 | } 380 | } 381 | updateCommentsAndActions(CommentsArray); 382 | } 383 | else { 384 | updateCommentsAndActions(CommentsArray); 385 | } 386 | } 387 | 388 | function onQueryCommentsError(commentRequest) { 389 | showFatalError("onQueryCommentsError: \n\n" + commentRequest.responseText); 390 | } 391 | 392 | /* GitHub: Render markdown */ 393 | 394 | function renderMarkdown(markdown) { 395 | renderUrl = "https://api.github.com/markdown"; 396 | markdownBundle = { 397 | text: markdown, 398 | mode: "gfm", 399 | context: gpgc.repo_id 400 | }; 401 | postGitHubApiRequestWithCompletion( 402 | renderUrl, 403 | JSON.stringify(markdownBundle), 404 | AccessToken, 405 | onRenderRequestStarted, 406 | onMarkdownRendered, 407 | onMarkdownRenderError 408 | ); 409 | } 410 | 411 | function onRenderRequestStarted() { 412 | PreviewDiv.innerHTML = "

Rendering...

"; 413 | } 414 | 415 | function onMarkdownRendered(renderRequest) { 416 | var renderedHtml = renderRequest.responseText; 417 | PreviewDiv.innerHTML = renderedHtml; 418 | } 419 | 420 | function onMarkdownRenderError(renderRequest) { 421 | var helpErrorMessage = "Sorry, something surprising happened. Please try again."; 422 | var isRawHtml = false; 423 | if (gpgc.enable_diagnostics) { 424 | helpErrorMessage = "

gpgc Error: Render Failed

Could not render comment markdown

GitHub response:

" + renderRequest.responseText + "

"; 425 | isRawHtml = true; 426 | } 427 | 428 | showCommentHelpError(helpErrorMessage, isRawHtml); 429 | return; 430 | } 431 | 432 | /* GitHub: Post comment */ 433 | 434 | function postComment() { 435 | if (CommentMarkdown.value.length === 0) { 436 | showCommentHelpError("Sorry, but your comment is empty. Please try again.", /* isRawHtml: */ false); 437 | return; 438 | } else { 439 | clearCommentHelp(); 440 | } 441 | 442 | var createCommentJson = { body: CommentMarkdown.value }; 443 | postGitHubApiRequestWithCompletion( 444 | CommentsUrl, 445 | JSON.stringify(createCommentJson), 446 | AccessToken, 447 | onPostCommentStarted, 448 | onCommentPosted, 449 | onPostCommentError 450 | ); 451 | } 452 | 453 | function onPostCommentStarted() { 454 | showCommentHelpMessage("Posting comment..."); 455 | } 456 | 457 | function onCommentPosted(postCommentRequest) { 458 | var commentInformation = JSON.parse(postCommentRequest.responseText); 459 | var newComment = formatComment(commentInformation.user.avatar_url, commentInformation.user.html_url, commentInformation.user.login, commentInformation.body_html, commentInformation.updated_at); 460 | AllCommentsDiv.innerHTML += newComment; 461 | showAllComments(); 462 | updateCommentFormMode(WriteMode, /* reset: */ true); 463 | clearCommentHelp(); 464 | } 465 | 466 | function onPostCommentError(postCommentRequest) { 467 | var helpErrorMessage = "Sorry, something surprising happened. Please try again."; 468 | var isRawHtml = false; 469 | if (gpgc.enable_diagnostics) { 470 | helpErrorMessage = "

gpgc Error: Comment Failed

Could not create a new comment

GitHub response:

" + postCommentRequest.responseText + "

"; 471 | isRawHtml = true; 472 | } 473 | 474 | showCommentHelpError(helpErrorMessage, isRawHtml); 475 | } 476 | 477 | /* Comments */ 478 | 479 | function updateCommentsAndActions(allComments) { 480 | var elementsToShow = []; 481 | var elementsToHide = []; 482 | 483 | if (allComments.length === 0) { 484 | elementsToShow.push(NoCommentsDiv); 485 | } else { 486 | var allCommentsHtml = formatAllComments(CommentsArray); 487 | AllCommentsDiv.innerHTML = allCommentsHtml + AllCommentsDiv.innerHTML; 488 | 489 | var commentOrComments = allComments.length === 1 ? "Comment" : "Comments"; 490 | ShowCommentsButton.innerHTML = "Show " + allComments.length + " " + commentOrComments; 491 | 492 | if (typeof gpgc.use_show_action === "boolean" && gpgc.use_show_action) { 493 | elementsToShow.push(ActionsDiv); 494 | elementsToHide.push(AllCommentsDiv); 495 | } else { 496 | elementsToHide.push(ActionsDiv); 497 | elementsToShow.push(AllCommentsDiv); 498 | } 499 | } 500 | 501 | updateElements(elementsToShow, elementsToHide, /* elementsToEnable: */ null, /* elementsToDisable: */ null); 502 | } 503 | 504 | function formatAllComments(allComments) { 505 | var allCommentsHtml = ""; 506 | for (var i = 0; i < allComments.length; i++) { 507 | var user = allComments[i].user; 508 | allCommentsHtml += formatComment(user.avatar_url, user.html_url, user.login, allComments[i].body_html, allComments[i].updated_at); 509 | } 510 | 511 | return allCommentsHtml; 512 | } 513 | 514 | function formatComment(userAvatarUrl, userHtmlUrl, userLogin, commentBodyHtml, commentTimeStamp) { 515 | var commentDate = new Date(commentTimeStamp); 516 | var shortMonth = ShortMonthForIndex[commentDate.getMonth()]; 517 | var commentHtml = "
"; 518 | commentHtml += "
"; 519 | commentHtml += "" + userLogin + " "; 520 | commentHtml += "on " + commentDate.getDate() + " " + shortMonth + " " + commentDate.getFullYear() + ""; 521 | commentHtml += "
"; 522 | commentHtml += "
" + commentBodyHtml + "
"; 523 | commentHtml += "
"; 524 | return commentHtml; 525 | } 526 | 527 | function showAllComments(allComments) { 528 | var elementsToShow = [ AllCommentsDiv ]; 529 | var elementsToHide = [ ActionsDiv, NoCommentsDiv ]; 530 | updateElements(elementsToShow, elementsToHide, /* elementsToEnable: */ null, /* elementsToDisable: */ null); 531 | } 532 | 533 | /* Help and error messages */ 534 | 535 | function showCommentHelpMessage(message) { 536 | showCommentHelp(message, /* isRawHtml: */ false, "gpgc-help-message", "gpgc-help-error"); 537 | } 538 | 539 | function showCommentHelpError(error, isRawHtml) { 540 | showCommentHelp(error, isRawHtml, "gpgc-help-error", "gpgc-help-message"); 541 | } 542 | 543 | function showCommentHelp(message, isRawHtml, cssClassToAdd, cssClassToRemove) { 544 | if (isRawHtml) { 545 | HelpMessageDiv.innerHTML = message; 546 | } else { 547 | HelpMessageDiv.innerHTML = "

" + message + "

"; 548 | } 549 | HelpMessageDiv.classList.add(cssClassToAdd); 550 | HelpMessageDiv.classList.remove(cssClassToRemove); 551 | showElement(HelpMessageDiv); 552 | } 553 | 554 | function clearCommentHelp() { 555 | HelpMessageDiv.innerHTML = ""; 556 | HelpMessageDiv.classList.remove("gpgc-help-message"); 557 | HelpMessageDiv.classList.remove("gpgc-help-error"); 558 | hideElement(HelpMessageDiv); 559 | } 560 | 561 | function showFatalError(internalMessage) { 562 | var nextStepMessage = "

If you're the site owner, please set enable_diagnostics to true in _data/gpgc.yml to see more details.

"; 563 | if (gpgc.enable_diagnostics) { 564 | nextStepMessage = "

If you're the site owner, please contact ghpages-ghcomments for help.

Internal message

" + internalMessage + "
"; 565 | } 566 | 567 | ErrorDiv.innerHTML += "

Oops!

Something surprising happened.

" + nextStepMessage; 568 | showElement(ErrorDiv); 569 | } 570 | 571 | /* Visual element manipulation */ 572 | 573 | function updateElements(elementsToShow, elementsToHide, elementsToEnable, elementsToDisable) { 574 | if (elementsToShow !== null) { showElements(elementsToShow); } 575 | if (elementsToHide !== null) { hideElements(elementsToHide); } 576 | if (elementsToEnable !== null) { enableElements(elementsToEnable); } 577 | if (elementsToDisable !== null) { disableElements(elementsToDisable); } 578 | } 579 | 580 | function updateElementVisibility(element, makeVisible) { 581 | if (makeVisible) { 582 | element.classList.remove("gpgc-hidden"); 583 | } else { 584 | element.classList.add("gpgc-hidden"); 585 | } 586 | } 587 | 588 | function showElement(element) { 589 | updateElementVisibility(element, /* makeVisible: */ true); 590 | } 591 | 592 | function showElements(elementList) { 593 | for (var i = 0; i < elementList.length; i++) { 594 | showElement(elementList[i]); 595 | } 596 | } 597 | 598 | function hideElement(element) { 599 | updateElementVisibility(element, /* makeVisible: */ false); 600 | } 601 | 602 | function hideElements(elementList) { 603 | for (var i = 0; i < elementList.length; i++) { 604 | hideElement(elementList[i]); 605 | } 606 | } 607 | 608 | function updateElementInteractivity(element, makeInteractive) { 609 | if (makeInteractive) { 610 | element.disabled = false; 611 | } else { 612 | element.disabled = true; 613 | } 614 | } 615 | 616 | function enableElement(elementToEnable) { 617 | updateElementInteractivity(elementToEnable, /* makeInteractive: */ true); 618 | } 619 | 620 | function enableElements(elementList) { 621 | for (var i = 0; i < elementList.length; i++) { 622 | enableElement(elementList[i]); 623 | } 624 | } 625 | 626 | function disableElement(elementToDisable) { 627 | updateElementInteractivity(elementToDisable, /* makeInteractive: */ false); 628 | } 629 | 630 | function disableElements(elementList) { 631 | for (var i = 0; i < elementList.length; i++) { 632 | disableElement(elementList[i]); 633 | } 634 | } 635 | 636 | /* Async web requests */ 637 | 638 | function getGitHubApiRequestWithCompletion(url, data, accessToken, onPreRequest, onSuccess, onError) { 639 | doGitHubApiRequestWithCompletion("GET", url, data, accessToken, onPreRequest, onSuccess, onError); 640 | } 641 | 642 | function postGitHubApiRequestWithCompletion(url, data, accessToken, onPreRequest, onSuccess, onError) { 643 | doGitHubApiRequestWithCompletion("POST", url, data, accessToken, onPreRequest, onSuccess, onError); 644 | } 645 | 646 | function doGitHubApiRequestWithCompletion(method, url, data, accessToken, onPreRequest, onSuccess, onError) { 647 | var gitHubRequest = new XMLHttpRequest(); 648 | gitHubRequest.open(method, url, /* async: */ true); 649 | 650 | if (accessToken !== null && accessToken !== "") { 651 | gitHubRequest.setRequestHeader("Authorization", "token " + accessToken); 652 | } 653 | 654 | gitHubRequest.setRequestHeader("Accept", "application/vnd.github.v3.html+json"); 655 | gitHubRequest.onreadystatechange = function () { onRequestReadyStateChange(gitHubRequest, onSuccess, onError); }; 656 | 657 | onPreRequest(); 658 | gitHubRequest.send(data); 659 | } 660 | 661 | function noop() { 662 | } 663 | 664 | function onRequestReadyStateChange(httpRequest, onSuccess, onError) { 665 | if (httpRequest.readyState !== 4) { return; } 666 | if (httpRequest.status === 200 || httpRequest.status === 201) { 667 | onSuccess(httpRequest); 668 | } else { 669 | onError(httpRequest); 670 | } 671 | } 672 | 673 | /* Diagnostics */ 674 | 675 | function verifyCss() { 676 | var css = document.styleSheets; 677 | var foundCssInHead = false; 678 | var fetchedCss = false; 679 | for (var i = 0; i < css.length; i++) { 680 | if (css[i].href.match("gpgc_styles.css")) { 681 | foundCssInHead = true; 682 | if (css[i].cssRules.length > 0) { 683 | fetchedCss = true; 684 | } 685 | break; 686 | } 687 | } 688 | 689 | var missingCssMessage = ""; 690 | if (! foundCssInHead) { 691 | missingCssMessage = "

gpgc Error: Missing CSS

gpgc_styles.css is not in the <head> element.

Add a <link> element to _includes/head.hml.

"; 692 | } 693 | 694 | var css404Message = ""; 695 | if (! fetchedCss && foundCssInHead) { 696 | css404Message = "

gpgc Error: CSS 404

Could not retrieve gpgc_styles.css from your site.

Check _includes/head.hml for typos.

"; 697 | } 698 | 699 | var allMessagesHtml = missingCssMessage + css404Message; 700 | showGeneralHelp(allMessagesHtml); 701 | } 702 | 703 | function verifyInitialConditions() { 704 | var missingPropertyMessage = "

gpgc Error: Incomplete Configuration

The following settings are missing:

"; 705 | var stringPropertyNames = ["site_url", "page_path", "issue_title", "repo_id", "github_application_client_id", "github_application_code_authenticator_url", "github_application_login_redirect_url"]; 706 | var booleanPropertyNames = ["new_comments_disabled", "use_show_action", "enable_diagnostics"]; 707 | var missingPropertyCounter = 0; 708 | 709 | for (var index in stringPropertyNames) { 710 | var stringProperty = stringPropertyNames[index]; 711 | if (gpgc[stringProperty].length < 1) { 712 | if (missingPropertyCounter == 0) { 713 | missingPropertyMessage = missingPropertyMessage + "
    "; 714 | } 715 | 716 | missingPropertyMessage = missingPropertyMessage + "
  1. " + stringProperty + "
  2. "; 717 | missingPropertyCounter++; 718 | } 719 | } 720 | 721 | for (var index in booleanPropertyNames) { 722 | var booleanProperty = booleanPropertyNames[index]; 723 | if (typeof(gpgc[booleanProperty]) != "boolean") { 724 | if (missingPropertyCounter == 0) { 725 | missingPropertyMessage = missingPropertyMessage + "
      "; 726 | } 727 | 728 | missingPropertyMessage = missingPropertyMessage + "
    1. " + booleanProperty + "
    2. "; 729 | missingPropertyCounter++; 730 | } 731 | } 732 | 733 | if (missingPropertyCounter > 0) { 734 | missingPropertyMessage = missingPropertyMessage + "
    "; 735 | showGeneralHelp(missingPropertyMessage); 736 | } 737 | } 738 | 739 | function showGeneralHelp(allMessagesHtml) { 740 | if (allMessagesHtml.length > 0) { 741 | allMessagesHtml += "

    Help

    Verify your site's configuration with the setup instructions and refer to the verbose usage for step-by-step details.

    Contact ghpages-ghcomments for more help.

    "; 742 | 743 | ErrorDiv.innerHTML += allMessagesHtml; 744 | showElement(ErrorDiv); 745 | } 746 | } 747 | --------------------------------------------------------------------------------