You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/10/20 11:02:54 UTC
(skywalking-website) 01/01: Add the Certificate of Contribution
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch feat/certificate
in repository https://gitbox.apache.org/repos/asf/skywalking-website.git
commit daded30c94898e9d46f252078c915f6e97d46bd3
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Tue Oct 20 19:02:42 2026 +0800
Add the Certificate of Contribution
Every PMC member, committer and contributor listed on the site can get an
Apache SkyWalking certificate at /certificate/. It is drawn in the browser
from data the build publishes, downloads as PNG and prints on one A4 page.
- data/repositories.yml catalogs every counted repository into eight
contribution categories; validation fails while a repository is missing,
duplicated or stale. The certificate's contribution polygon has one vertex
per category, placed by the person's rank in it in 10% bands.
- scripts/certificate.js builds the page data and each day's list of issued
certificate codes (certificate/verify/<date>.txt). A certificate ID is the
date plus an 80-bit SHA-256 code over its facts; its QR code carries both,
and the page verifies them against that day's list.
- The deploy carries certificate/verify/ forward on asf-site, and only that
folder; deploying runs share one concurrency group.
- Team page certificate badge, a certificate button on contributor pages, and
a full-width contributors page header.
---
.github/workflows/deploy.yaml | 27 +
.gitignore | 3 +
CLAUDE.md | 33 +
README.md | 12 +
assets/js/certificate/designs/claude.js | 425 ++++
assets/js/certificate/model.js | 148 ++
assets/js/certificate/page.js | 293 +++
assets/js/certificate/runtime.js | 154 ++
assets/scss/_custom_certificate.scss | 105 +
assets/scss/_custom_contributors.scss | 6 +
assets/scss/_custom_team.scss | 23 +
content/certificate/_index.md | 7 +
data/repositories.yml | 92 +
layouts/certificate/baseof.html | 23 +
layouts/contributors/list.html | 4 +
layouts/shortcodes/certificate.html | 60 +
layouts/shortcodes/contributors-search.html | 1 +
layouts/team/list.html | 34 +-
package.json | 10 +-
scripts/certificate.js | 104 +
scripts/certificate.test.js | 96 +
scripts/project-config.js | 36 +-
scripts/project-config.test.js | 25 +-
.../certificate/asf-logo-horizontal-color.svg | 43 +
themes/docsy/assets/scss/_styles_project.scss | 1 +
themes/docsy/layouts/partials/script-search.html | 1 +
themes/docsy/static/js/qrcode-generator-1.4.4.js | 2297 ++++++++++++++++++++
27 files changed, 4048 insertions(+), 15 deletions(-)
diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index 87c484f18f28..7ce2ae39ac08 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -27,6 +27,12 @@ on:
jobs:
build:
runs-on: ubuntu-latest
+ # A deploy replaces the whole asf-site tree, carrying certificate/verify/ forward from the
+ # tree it fetched. Two deploys at once could drop each other's certificate records, so
+ # deploying runs queue in one group; pull request builds keep their own groups.
+ concurrency:
+ group: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.ref) || 'asf-site-deploy' }}
+ cancel-in-progress: false
permissions:
contents: write
env:
@@ -48,6 +54,27 @@ jobs:
# from 60/hr (anonymous) to 1,000/hr.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # The deploy below replaces the whole asf-site tree. The daily lists of issued certificate
+ # codes (certificate/verify/<date>.txt) are what keeps a certificate verifiable, so this
+ # one folder is carried forward from asf-site and merged with today's list. Nothing else
+ # is kept. Only that folder's files are downloaded (blobless fetch); any failure stops the
+ # deploy rather than publishing without the records.
+ - name: Keep issued certificate records
+ if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
+ run: |
+ git fetch --depth=1 --filter=blob:none origin asf-site
+ if git cat-file -e FETCH_HEAD:certificate/verify 2>/dev/null; then
+ kept=$(mktemp -d)
+ git --work-tree="$kept" restore --source=FETCH_HEAD --worktree -- certificate/verify
+ mkdir -p public/certificate/verify
+ for file in "$kept"/certificate/verify/*; do
+ target="public/certificate/verify/$(basename "$file")"
+ # Byte order keeps the '#' category header first and merges identical headers.
+ if [ -f "$target" ]; then LC_ALL=C sort -u "$file" "$target" -o "$target"; else cp "$file" "$target"; fi
+ done
+ echo "Kept $(ls "$kept"/certificate/verify | wc -l) daily certificate records."
+ fi
+
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
diff --git a/.gitignore b/.gitignore
index 97ea3db4423e..311652f69860 100755
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,9 @@
/tmp
/data/docSidebar/*
/data/team.yml
+# Certificate data, built by scripts/certificate.js
+/assets/certificate/
+/static/certificate/verify/
/themes/docsy/static/js/mergedData.js
!.gitkeep
/content/docs/*
diff --git a/CLAUDE.md b/CLAUDE.md
index b74411401c50..806dd31160e1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -44,6 +44,39 @@ from `data/projects.yml`. Its `showcase` block contains Showcase metadata,
See the complete YAML example in README.md. Run `node scripts/project-config.js`
for local configuration validation without fetching repositories or artifacts.
+`data/repositories.yml` catalogs every repository whose contributors the site
+counts — each repository of `data/projects.yml`, the Showcase and the Website —
+into the contribution categories of the Certificate of Contribution
+(`/certificate/`). **When a repository is added to the site, add it as
+`owner/repo` to exactly one category there in the same change.** The validator
+rejects missing, duplicate and stale entries, and it runs inside every build.
+Category order is the vertex order of the certificate's contribution polygon.
+
+### Certificate of Contribution data
+
+`scripts/certificate.js` runs in every build: after `npm run team` in
+`build-with-docs` (the deploy) and `build-preview`; `npm run build` has no
+contributor data, so its certificates list members only. Locally, run
+`npm run team` (needs `GITHUB_TOKEN`) and then `npm run certificate` before
+`hugo server`. It writes two gitignored files:
+
+- `assets/certificate/people.json`: today's data the `/certificate/` page draws
+ from. It is regenerated every build and never kept.
+- `static/certificate/verify/<yyyy-mm-dd>.txt`: a `#` header naming that day's
+ categories in polygon order, then the code of every certificate that can be
+ issued that day. A certificate's verify link carries its date, code and facts;
+ the page recomputes the code, looks it up in that day's list, and labels the
+ polygon with that day's categories.
+
+`asf-site` is replaced on every deploy, except `certificate/verify/`: the
+"Keep issued certificate records" step in `.github/workflows/deploy.yaml`
+carries that one folder forward and merges same-day lists; deploying runs share one
+concurrency group so they cannot drop each other's records. **Never delete or
+rename it, and never carry any other folder forward.** The code format lives in
+`assets/js/certificate/model.js` (`canonical`, `encode`); changing it breaks
+every issued certificate, so a new format needs a new `VERSION` while `v1`
+links keep verifying.
+
## Releases in data/projects.yml
Each release has `version` and a boolean `latest`. Keep exactly one `latest: true`
diff --git a/README.md b/README.md
index 6a4e34fdaefb..287a078f4e83 100755
--- a/README.md
+++ b/README.md
@@ -82,6 +82,18 @@ available. Each project's `dockerImages` array sits alongside `releases` and
contains named Docker Hub repository links with optional descriptions. Images
remain available independently of the selected release; they are repository
links rather than version-pinned image tags.
+`data/repositories.yml` catalogs every repository whose contributors the site counts
+(each repository of `data/projects.yml`, the Showcase and the Website) into the
+contribution categories of the Certificate of Contribution at `/certificate/`. Add a
+new repository there, as `owner/repo` under one category, in the same change that adds
+it to the site; validation fails while any repository is missing, duplicated or stale.
+`scripts/certificate.js` (part of every build) builds the certificate page's data and
+the day's list of issued certificate codes, `certificate/verify/<date>.txt`. It reads
+the contributor data that `npm run team` fetches, so `build-with-docs` and
+`build-preview` run it after `npm run team`; `npm run build` has no contributor data
+and lists members only. The deploy keeps `certificate/verify/` on `asf-site` across
+deploys, so issued certificates stay verifiable; everything else is replaced. Locally,
+run `npm run team` (with `GITHUB_TOKEN`) and `npm run certificate` before `hugo server`.
Run `node scripts/project-config.js` to validate the configuration locally without
fetching documentation or release artifacts.
diff --git a/assets/js/certificate/designs/claude.js b/assets/js/certificate/designs/claude.js
new file mode 100644
index 000000000000..32deba98fae5
--- /dev/null
+++ b/assets/js/certificate/designs/claude.js
@@ -0,0 +1,425 @@
+/*
+ * "Contribution Sky Map" — Apache SkyWalking certificate design.
+ *
+ * Left: a light certificate in the website's hero wash. Right: a night-sky panel with the
+ * recipient's contribution polygon over the repository categories of data/repositories.yml.
+ */
+(function () {
+ 'use strict';
+
+ window.SWCert = window.SWCert || {};
+ SWCert.designs = SWCert.designs || {};
+
+ var W = 1123, H = 794;
+ var INK = '#172b40', BODY = '#4b5563', MUTED = '#8a93a3', LINE = '#e6e9ee', BRAND = '#3788D0';
+ var LEFT = 56, CONTENT_W = 600;
+ var PANEL_X = 700, PANEL_W = W - 16 - PANEL_X; // 407
+ var CX = PANEL_X + PANEL_W / 2, CY = 286, R = 98; // contribution polygon centre and radius
+
+ var TIERS = {
+ 'PMC Member': { from: '#28265E', to: '#7C297D', solid: '#7C297D', glow: '#d9a8f0' },
+ 'Committer': { from: '#2875b9', to: '#479EEB', solid: '#2875b9', glow: '#9cc9f5' },
+ 'Contributor': { from: '#DD552C', to: '#F79A23', solid: '#DD552C', glow: '#ffd29a' }
+ };
+ var TRADEMARK = 'Apache SkyWalking, SkyWalking, Apache, the Apache feather logo, and the Apache ' +
+ 'SkyWalking project logo are either registered trademarks or trademarks of the Apache Software Foundation.';
+
+ function num(n) { return Number(n).toLocaleString('en-US'); }
+ function f(n) { return Math.round(n * 100) / 100; }
+
+ function hash(s) { // FNV-1a → [0, 1)
+ var h = 2166136261;
+ for (var i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
+ return ((h >>> 0) % 100000) / 100000;
+ }
+
+ function wrap(text, font, maxWidth, ctx) {
+ var words = text.split(' '), lines = [], line = '';
+ words.forEach(function (w) {
+ var next = line ? line + ' ' + w : w;
+ if (line && ctx.measure(next, font) > maxWidth) { lines.push(line); line = w; } else { line = next; }
+ });
+ if (line) lines.push(line);
+ return lines;
+ }
+
+ function textLines(lines, x, y, lh, attrs, ctx) {
+ return '<text x="' + x + '" y="' + y + '" ' + attrs + '>' + lines.map(function (l, i) {
+ return '<tspan x="' + x + '" dy="' + (i ? lh : 0) + '">' + ctx.escape(l) + '</tspan>';
+ }).join('') + '</text>';
+ }
+
+ function statement(p) {
+ if (p.role === 'PMC Member') {
+ return 'in recognition of stewardship as a member of the Apache SkyWalking Project Management ' +
+ 'Committee, and of contributions to Apache SkyWalking, an open source observability platform ' +
+ 'of The Apache Software Foundation.';
+ }
+ if (p.role === 'Committer') {
+ return 'in recognition of earning commit access to Apache SkyWalking, the open source ' +
+ 'observability platform of The Apache Software Foundation.';
+ }
+ return 'in recognition of contributions to Apache SkyWalking, the open source observability ' +
+ 'platform of The Apache Software Foundation.';
+ }
+
+ // Contribution polygon: one vertex per category of data/repositories.yml (ctx.catalogs, in
+ // config order), clockwise from the top. A vertex's distance is the recipient's rank among
+ // everyone who contributed to that category, in ten 10% bands: the top 10% reach the outer
+ // ring, the next 10% the ninth ring, and so on; categories without contributions sit at the
+ // centre.
+ function band(c) {
+ if (c.band != null) return c.band; // computed by SWCertModel
+ return c.contributions ? 10 - Math.floor((c.rank - 1) * 10 / c.of) : 0;
+ }
+
+ function polygon(p, ctx, tier, namesOnly) {
+ var cats = ctx.catalogs, n = cats.length, out = [];
+ var mineBy = {};
+ (p.categories || []).forEach(function (c) { mineBy[c.id] = c; });
+ var stat = cats.map(function (c) { return mineBy[c.id] || { contributions: 0, rank: null, of: 0 }; });
+ var any = stat.some(function (c) { return c.contributions > 0; });
+ function angle(k) { return -Math.PI / 2 + k * 2 * Math.PI / n; }
+ function at(k, r) { return [CX + Math.cos(angle(k)) * r, CY + Math.sin(angle(k)) * r]; }
+ function level(k) { return band(stat[k]) / 10; }
+ function ring(k) {
+ return cats.map(function (c, i) { var q = at(i, R * k); return f(q[0]) + ',' + f(q[1]); }).join(' ');
+ }
+
+ // Frame: one ring per 10% band, the halfway ring a little stronger, and the axes.
+ for (var b = 1; b < 10; b++) {
+ out.push('<polygon points="' + ring(b / 10) + '" fill="none" stroke="#ffffff" stroke-opacity="' + (b === 5 ? '.14' : '.06') + '"/>');
+ }
+ out.push('<polygon points="' + ring(1) + '" fill="#ffffff" fill-opacity=".03" stroke="#ffffff" stroke-opacity=".22"/>');
+ cats.forEach(function (c, k) {
+ var q = at(k, R);
+ out.push('<line x1="' + f(CX) + '" y1="' + CY + '" x2="' + f(q[0]) + '" y2="' + f(q[1]) + '" stroke="#ffffff" stroke-opacity=".08"/>');
+ });
+
+ // The recipient's polygon.
+ if (any) {
+ var pts = cats.map(function (c, k) { var q = at(k, R * level(k)); return f(q[0]) + ',' + f(q[1]); }).join(' ');
+ out.push('<polygon points="' + pts + '" fill="url(#claude-poly)" stroke="' + tier.glow + '" stroke-width="1.6" stroke-linejoin="round"/>');
+ cats.forEach(function (c, k) {
+ var v = stat[k].contributions, q = at(k, R * level(k));
+ if (v) {
+ out.push('<circle cx="' + f(q[0]) + '" cy="' + f(q[1]) + '" r="10" fill="url(#claude-glow)"/>');
+ out.push('<circle cx="' + f(q[0]) + '" cy="' + f(q[1]) + '" r="3.4" fill="#ffffff"/>');
+ }
+ });
+ }
+
+ // The recipient at the centre.
+ out.push('<circle cx="' + f(CX) + '" cy="' + CY + '" r="7" fill="url(#claude-tier)" stroke="#ffffff" stroke-opacity=".9" stroke-width="1.2"/>');
+ out.push('<circle cx="' + f(CX) + '" cy="' + CY + '" r="2" fill="#ffffff"/>');
+
+ // Category labels: name (wrapped when long and set beside the polygon), count, and rank.
+ var LABEL = '700 9px ' + ctx.fonts.sans, TRACK = 0.8, MIN_X = PANEL_X + 14, MAX_X = PANEL_X + PANEL_W - 14;
+ function width(text, font, track) { return ctx.measure(text, font) + (track || 0) * text.length; }
+ cats.forEach(function (c, k) {
+ var a = angle(k), cos = Math.cos(a), sin = Math.sin(a), v = stat[k].contributions;
+ var anchor = cos > 0.35 ? 'start' : (cos < -0.35 ? 'end' : 'middle');
+ var name = c.name.toUpperCase(), lines = [name];
+ if (anchor !== 'middle' && width(name, LABEL, TRACK) > 84 && name.indexOf(' ') > 0) {
+ var words = name.split(' '), best = null;
+ for (var i = 1; i < words.length; i++) {
+ var l1 = words.slice(0, i).join(' '), l2 = words.slice(i).join(' ');
+ var wmax = Math.max(width(l1, LABEL, TRACK), width(l2, LABEL, TRACK));
+ if (!best || wmax < best.w) best = { w: wmax, lines: [l1, l2] };
+ }
+ lines = best.lines;
+ }
+ var count = namesOnly ? '' : (v ? num(v) : '—'), rank = v && !namesOnly ? '#' + num(stat[k].rank) + ' of ' + num(stat[k].of) : '';
+ var w = Math.max.apply(null, lines.map(function (l) { return width(l, LABEL, TRACK); })
+ .concat([width(count, '9.5px ' + ctx.fonts.mono), width(rank, '9px ' + ctx.fonts.mono)]));
+ var q = at(k, R + 15), x = q[0];
+ if (anchor === 'start') x = Math.min(x, MAX_X - w);
+ if (anchor === 'end') x = Math.max(x, MIN_X + w);
+ if (anchor === 'middle') x = Math.min(Math.max(x, MIN_X + w / 2), MAX_X - w / 2);
+ var lh = 11, block = (lines.length + (namesOnly ? 0 : (v ? 2 : 1))) * lh;
+ var top = sin < -0.7 ? q[1] - block + 4 : (sin > 0.7 ? q[1] + 6 : q[1] - block / 2 + 8);
+ var lit = v ? '.92' : '.42';
+ lines.forEach(function (l, i) {
+ out.push('<text x="' + f(x) + '" y="' + f(top + i * lh) + '" text-anchor="' + anchor + '" font-family=\'' + ctx.fonts.sans +
+ '\' font-size="9" font-weight="700" letter-spacing="' + TRACK + '" fill="#ffffff" fill-opacity="' + lit + '">' + ctx.escape(l) + '</text>');
+ });
+ if (namesOnly) return;
+ out.push('<text x="' + f(x) + '" y="' + f(top + lines.length * lh) + '" text-anchor="' + anchor + '" font-family=\'' + ctx.fonts.mono +
+ '\' font-size="9.5" fill="' + (v ? tier.glow : '#ffffff') + '" fill-opacity="' + (v ? '1' : '.4') + '">' + ctx.escape(count) + '</text>');
+ if (v) {
+ out.push('<text x="' + f(x) + '" y="' + f(top + (lines.length + 1) * lh) + '" text-anchor="' + anchor + '" font-family=\'' + ctx.fonts.mono +
+ '\' font-size="9" fill="#ffffff" fill-opacity=".55">' + ctx.escape(rank) + '</text>');
+ }
+ });
+ return out.join('');
+ }
+
+ // The certificate's paper, night-sky panel and frame, shared by the certificate and its
+ // verification record. extraDefs adds definitions of the caller's own.
+ function shell(tier, extraDefs) {
+ var s = [];
+ s.push('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H + '">');
+ s.push('<defs>' +
+ '<radialGradient id="claude-wash-blue" cx="10%" cy="0%" r="70%"><stop offset="0" stop-color="#3788D0" stop-opacity=".13"/><stop offset="1" stop-color="#3788D0" stop-opacity="0"/></radialGradient>' +
+ '<radialGradient id="claude-wash-pink" cx="62%" cy="0%" r="45%"><stop offset="0" stop-color="#e84ea1" stop-opacity=".08"/><stop offset="1" stop-color="#e84ea1" stop-opacity="0"/></radialGradient>' +
+ '<linearGradient id="claude-paper" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f7faff"/><stop offset=".7" stop-color="#ffffff"/></linearGradient>' +
+ '<linearGradient id="claude-sky" x1="0" y1="0" x2=".35" y2="1"><stop offset="0" stop-color="#081830"/><stop offset=".55" stop-color="#0e2747"/><stop offset="1" stop-color="#16375f"/></linearGradient>' +
+ '<radialGradient id="claude-sky-glow" cx="50%" cy="36%" r="55%"><stop offset="0" stop-color="#3788D0" stop-opacity=".30"/><stop offset="1" stop-color="#3788D0" stop-opacity="0"/></radialGradient>' +
+ '<radialGradient id="claude-glow"><stop offset="0" stop-color="' + tier.glow + '" stop-opacity=".75"/><stop offset="1" stop-color="' + tier.glow + '" stop-opacity="0"/></radialGradient>' +
+ '<radialGradient id="claude-poly" gradientUnits="userSpaceOnUse" cx="' + f(CX) + '" cy="' + CY + '" r="' + R + '"><stop offset="0" stop-color="' + tier.glow + '" stop-opacity=".42"/><stop offset="1" stop-color="' + tier.glow + '" stop-opacity=".16"/></radialGradient>' +
+ '<linearGradient id="claude-tier" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="' + tier.from + '"/><stop offset="1" stop-color="' + tier.to + '"/></linearGradient>' +
+ '<linearGradient id="claude-edge" gradientUnits="userSpaceOnUse" x1="' + f(CX - R) + '" y1="' + (CY - R) + '" x2="' + f(CX + R) + '" y2="' + (CY + R) + '"><stop offset="0" stop-color="#ffffff" stop-opacity=".55"/><stop offset="1" stop-color="' + tier.glow + '" stop-opacity=".55"/></linearGradient>' +
+ '<clipPath id="claude-frame"><rect x="16" y="16" width="' + (W - 32) + '" height="' + (H - 32) + '" rx="18"/></clipPath>' +
+ '<mask id="claude-moon"><rect x="' + (PANEL_X + PANEL_W - 70) + '" y="30" width="60" height="60" fill="#ffffff"/><circle cx="' + (PANEL_X + PANEL_W - 38) + '" cy="52" r="13" fill="#000000"/></mask>' +
+ extraDefs + '</defs>');
+
+ // Paper and frame.
+ s.push('<rect width="' + W + '" height="' + H + '" fill="#eef3f9"/>');
+ s.push('<g clip-path="url(#claude-frame)">');
+ s.push('<rect x="16" y="16" width="' + (W - 32) + '" height="' + (H - 32) + '" fill="url(#claude-paper)"/>');
+ s.push('<rect x="16" y="16" width="' + (W - 32) + '" height="' + (H - 32) + '" fill="url(#claude-wash-blue)"/>');
+ s.push('<rect x="16" y="16" width="' + (W - 32) + '" height="' + (H - 32) + '" fill="url(#claude-wash-pink)"/>');
+
+ // Night-sky panel.
+ s.push('<rect x="' + PANEL_X + '" y="16" width="' + PANEL_W + '" height="' + (H - 32) + '" fill="url(#claude-sky)"/>');
+ s.push('<rect x="' + PANEL_X + '" y="16" width="' + PANEL_W + '" height="' + (H - 32) + '" fill="url(#claude-sky-glow)"/>');
+ for (var k = 0; k < 70; k++) { // fixed background starfield, off the chart
+ var sx = PANEL_X + 10 + hash('x' + k) * (PANEL_W - 20), sy = 26 + hash('y' + k) * (H - 52);
+ if (Math.abs(sx - CX) < R + 120 && sy > CY - R - 70 && sy < CY + R + 100) continue;
+ s.push('<circle cx="' + f(sx) + '" cy="' + f(sy) + '" r="' + f(0.4 + hash('r' + k) * 0.8) + '" fill="#ffffff" fill-opacity="' + f(0.08 + hash('o' + k) * 0.22) + '"/>');
+ }
+ s.push('<circle cx="' + (PANEL_X + PANEL_W - 46) + '" cy="58" r="15" fill="#ffffff" fill-opacity=".85" mask="url(#claude-moon)"/>');
+ s.push('</g>');
+ s.push('<rect x="16.5" y="16.5" width="' + (W - 33) + '" height="' + (H - 33) + '" rx="18" fill="none" stroke="#d6e4f5"/>');
+
+
+ return s.join('');
+ }
+
+ // Issuer row, THE ASF lockup and the trademark line.
+ function footer(p, ctx) {
+ var s = [], sans = ctx.fonts.sans, mono = ctx.fonts.mono, e = ctx.escape;
+ // Issuer row.
+ var metaY = 662;
+ s.push('<line x1="' + LEFT + '" y1="' + (metaY - 24) + '" x2="' + (LEFT + CONTENT_W) + '" y2="' + (metaY - 24) + '" stroke="' + LINE + '"/>');
+ [['ISSUED BY', 'The Apache SkyWalking PMC', sans, 0], ['ISSUED ON', p.issued, sans, 236], ['CERTIFICATE ID', p.certId, mono, 400]].forEach(function (m) {
+ s.push('<text x="' + (LEFT + m[3]) + '" y="' + metaY + '" font-family=\'' + sans + '\' font-size="9.5" font-weight="700" letter-spacing="1.5" fill="' + MUTED + '">' + m[0] + '</text>');
+ s.push('<text x="' + (LEFT + m[3]) + '" y="' + (metaY + 20) + '" font-family=\'' + m[2] + '\' font-size="14" font-weight="700" fill="#1F1F20">' + e(m[1]) + '</text>');
+ });
+
+ // THE ASF lockup (official colour file, light ground, clear space kept) and trademark line.
+ var asfH = 52, asfW = asfH * 1000 / 525;
+ s.push('<image href="' + ctx.assets.asfLogo + '" x="' + (LEFT - 6) + '" y="' + (H - 16 - 22 - asfH) + '" width="' + f(asfW) + '" height="' + asfH + '"/>');
+ var tm = wrap(TRADEMARK, '8.5px ' + sans, CONTENT_W - asfW - 14, ctx);
+ s.push(textLines(tm, f(LEFT + asfW + 8), H - 16 - 22 - asfH + 22, 11.5, 'font-family=\'' + sans + '\' font-size="8.5" fill="' + MUTED + '"', ctx));
+
+ return s.join('');
+ }
+
+ function render(p, ctx) {
+ var tier = TIERS[p.role] || TIERS.Contributor;
+ var sans = ctx.fonts.sans, mono = ctx.fonts.mono, e = ctx.escape;
+ var hasData = p.total != null && p.projectCount > 0;
+ var areas = {};
+ (p.projects || []).forEach(function (pr) { areas[pr.catalog] = true; });
+ var areaCount = Object.keys(areas).length, repoCount = 0;
+ ctx.catalogs.forEach(function (c) { repoCount += c.repos.length; });
+ var s = [];
+
+ s.push(shell(tier, ''));
+
+ // Logo, eyebrow, recipient.
+ s.push('<image href="' + ctx.assets.skywalkingLogo + '" x="' + LEFT + '" y="60" width="169" height="40"/>');
+ s.push('<text x="' + LEFT + '" y="150" font-family=\'' + sans + '\' font-size="12.5" font-weight="700" letter-spacing="2.4" fill="' + BRAND + '">CERTIFICATE OF CONTRIBUTION</text>');
+ s.push('<text x="' + LEFT + '" y="184" font-family=\'' + sans + '\' font-size="15" fill="' + BODY + '">This certificate is presented to</text>');
+ var nameSize = ctx.fit(p.displayName, function (z) { return '800 ' + z + 'px ' + sans; }, CONTENT_W, 60, 28);
+ s.push('<text x="' + (LEFT - 2) + '" y="' + (196 + nameSize * 0.86) + '" font-family=\'' + sans + '\' font-size="' + nameSize + '" font-weight="800" letter-spacing="' + f(-nameSize * 0.02) + '" fill="' + INK + '">' + e(p.displayName) + '</text>');
+
+ // Tier pill and identity line.
+ var pillY = 280;
+ var pillText = (p.role + (p.roleNote ? ' · ' + p.roleNote : '')).toUpperCase();
+ var pillW = ctx.measure(pillText, '700 11.5px ' + sans) + 11.5 * 0.12 * pillText.length + 30;
+ s.push('<rect x="' + LEFT + '" y="' + pillY + '" width="' + f(pillW) + '" height="28" rx="14" fill="url(#claude-tier)"/>');
+ s.push('<text x="' + (LEFT + 15) + '" y="' + (pillY + 18.5) + '" font-family=\'' + sans + '\' font-size="11.5" font-weight="700" letter-spacing="1.4" fill="#ffffff">' + e(pillText) + '</text>');
+ var ident = p.kind === 'member'
+ ? 'ASF ID ' + p.apacheId + (p.login ? ' · GitHub @' + p.login : '')
+ : 'github.com/' + p.login;
+ s.push('<text x="' + f(LEFT + pillW + 14) + '" y="' + (pillY + 18.5) + '" font-family=\'' + mono + '\' font-size="12" fill="#536478">' + e(ident) + '</text>');
+
+ // Statement.
+ var stmt = wrap(statement(p), '15.5px ' + sans, CONTENT_W, ctx);
+ s.push(textLines(stmt, LEFT, 352, 24, 'font-family=\'' + sans + '\' font-size="15.5" fill="' + BODY + '"', ctx));
+
+ var statsY = 352 + stmt.length * 24 + 26;
+ if (hasData) {
+ // Stats row.
+ var stats = [
+ ['CONTRIBUTIONS', num(p.total), ''],
+ ['PROJECTS', String(p.projectCount), ' / ' + repoCount],
+ ['CATEGORIES', String(areaCount), ' / ' + ctx.catalogs.length]
+ ];
+ stats.forEach(function (st, i) {
+ var x = LEFT + i * 200;
+ s.push('<rect x="' + x + '" y="' + statsY + '" width="3" height="56" rx="1.5" fill="url(#claude-tier)"/>');
+ s.push('<text x="' + (x + 16) + '" y="' + (statsY + 14) + '" font-family=\'' + sans + '\' font-size="10.5" font-weight="700" letter-spacing="1.6" fill="' + MUTED + '">' + st[0] + '</text>');
+ s.push('<text x="' + (x + 15) + '" y="' + (statsY + 50) + '" font-family=\'' + mono + '\' font-size="34" font-weight="700" letter-spacing="-1" fill="' + INK + '">' + e(st[1]) +
+ '<tspan font-size="16" font-weight="500" fill="' + MUTED + '" letter-spacing="0">' + e(st[2]) + '</tspan></text>');
+ });
+
+ // Major contributions.
+ var cardsY = statsY + 92;
+ s.push('<text x="' + LEFT + '" y="' + cardsY + '" font-family=\'' + sans + '\' font-size="10.5" font-weight="700" letter-spacing="1.6" fill="' + MUTED + '">' +
+ (p.top.length > 1 ? 'MAJOR CONTRIBUTIONS' : 'MAJOR CONTRIBUTION') + '</text>');
+ p.top.forEach(function (t, i) {
+ var x = LEFT + i * 308, y = cardsY + 12, w = 292;
+ s.push('<rect x="' + x + '" y="' + y + '" width="' + w + '" height="62" rx="12" fill="#ffffff" stroke="' + LINE + '"/>');
+ var category = ctx.catalogs.filter(function (c) { return c.id === t.catalog; })[0];
+ s.push('<text x="' + (x + 60) + '" y="' + (y + 19) + '" font-family=\'' + sans + '\' font-size="8.5" font-weight="700" letter-spacing="1.2" fill="' + tier.solid + '">' +
+ e((category ? category.name : '').toUpperCase()) + '</text>');
+ var icon = t.icon && ctx.assets.projectIcons[t.icon];
+ if (icon) {
+ s.push('<image href="' + icon + '" x="' + (x + 15) + '" y="' + (y + 15) + '" width="32" height="32"/>');
+ } else {
+ s.push('<rect x="' + (x + 15) + '" y="' + (y + 15) + '" width="32" height="32" rx="9" fill="#eef5fd" stroke="#d6e4f5"/>');
+ s.push('<text x="' + (x + 31) + '" y="' + (y + 36) + '" text-anchor="middle" font-family=\'' + sans + '\' font-size="14" font-weight="800" fill="#2c6cb0">' + e(t.label.charAt(0)) + '</text>');
+ }
+ var labelSize = ctx.fit(t.label, function (z) { return '700 ' + z + 'px ' + sans; }, w - 76, 15, 11);
+ s.push('<text x="' + (x + 60) + '" y="' + (y + 35) + '" font-family=\'' + sans + '\' font-size="' + labelSize + '" font-weight="700" fill="#1F1F20">' + e(t.label) +
+ (t.archived ? '<tspan font-size="9.5" font-weight="600" fill="' + MUTED + '" letter-spacing=".8"> ARCHIVED</tspan>' : '') + '</text>');
+ s.push('<text x="' + (x + 60) + '" y="' + (y + 51) + '" font-family=\'' + mono + '\' font-size="11" fill="#536478">' +
+ e(num(t.contributions) + (t.contributions === 1 ? ' contribution' : ' contributions') + ' · #' + t.rank + ' of ' + num(t.of)) + '</text>');
+ });
+ } else {
+ // Members without GitHub data: a role card instead of stats.
+ var y0 = statsY;
+ s.push('<rect x="' + LEFT + '" y="' + y0 + '" width="' + CONTENT_W + '" height="92" rx="14" fill="#ffffff" stroke="' + LINE + '"/>');
+ s.push('<rect x="' + LEFT + '" y="' + y0 + '" width="4" height="92" rx="2" fill="url(#claude-tier)"/>');
+ s.push('<text x="' + (LEFT + 24) + '" y="' + (y0 + 30) + '" font-family=\'' + sans + '\' font-size="10.5" font-weight="700" letter-spacing="1.6" fill="' + MUTED + '">ROLE</text>');
+ s.push('<text x="' + (LEFT + 24) + '" y="' + (y0 + 60) + '" font-family=\'' + sans + '\' font-size="22" font-weight="800" fill="' + INK + '">' + e(p.role === 'PMC Member' ? 'Project Management Committee' : p.role) + '</text>');
+ s.push('<text x="' + (LEFT + 24) + '" y="' + (y0 + 78) + '" font-family=\'' + mono + '\' font-size="11" fill="#536478">' +
+ e('Listed at skywalking.apache.org/team/#' + p.apacheId) + '</text>');
+ }
+
+ s.push(footer(p, ctx));
+
+ // Sky panel content.
+ s.push('<text x="' + (PANEL_X + 28) + '" y="64" font-family=\'' + sans + '\' font-size="10.5" font-weight="700" letter-spacing="2" fill="#ffffff" fill-opacity=".8">CONTRIBUTION MAP</text>');
+ s.push('<text x="' + (PANEL_X + 28) + '" y="82" font-family=\'' + sans + '\' font-size="10.5" fill="#ffffff" fill-opacity=".5">By category, across all ' + repoCount + ' Apache SkyWalking repositories</text>');
+ s.push(polygon(p, ctx, tier));
+ var captionY = CY + R + 78;
+ var caption = hasData
+ ? p.projectCount + ' of ' + repoCount + ' repositories · ' + areaCount + ' of ' + ctx.catalogs.length + ' categories'
+ : repoCount + ' repositories in ' + ctx.catalogs.length + ' categories';
+ s.push('<text x="' + f(CX) + '" y="' + captionY + '" text-anchor="middle" font-family=\'' + mono + '\' font-size="10.5" fill="#ffffff" fill-opacity=".7">' + e(caption) + '</text>');
+
+ // QR tile.
+ var tile = 148, qrSize = 124, tx = CX - tile / 2, ty = 536;
+ s.push('<rect x="' + f(tx) + '" y="' + ty + '" width="' + tile + '" height="' + tile + '" rx="12" fill="#ffffff"/>');
+ s.push('<path d="' + ctx.qr(p.verifyUrl).path(tx + (tile - qrSize) / 2, ty + (tile - qrSize) / 2, qrSize) + '" fill="#0e2747"/>');
+ s.push('<text x="' + f(CX) + '" y="' + (ty + tile + 24) + '" text-anchor="middle" font-family=\'' + sans + '\' font-size="9.5" font-weight="700" letter-spacing="1.5" fill="#ffffff" fill-opacity=".6">SCAN TO VERIFY</text>');
+
+ s.push('</svg>');
+ return s.join('');
+ }
+
+ // A rubber stamp: the project's name on the upper ring, the issue date on the lower ring and
+ // VERIFIED across a band, rotated a little and inked unevenly.
+ var STAMP = '#1f8a52';
+ function stamp(cx, cy, issued, ctx) {
+ var sans = ctx.fonts.sans, r = 62, ar = r - 15, out = [];
+ out.push('<g transform="rotate(-12 ' + cx + ' ' + cy + ')" filter="url(#claude-ink)" fill="' + STAMP + '" stroke="' + STAMP + '">');
+ out.push('<circle cx="' + cx + '" cy="' + cy + '" r="' + r + '" fill="none" stroke-width="3.2"/>');
+ out.push('<circle cx="' + cx + '" cy="' + cy + '" r="' + (r - 6) + '" fill="none" stroke-width="1.1"/>');
+ // Upper arc runs clockwise (text sits outside the path), lower arc counter-clockwise, so
+ // both read left to right.
+ out.push('<path id="claude-stamp-top" fill="none" stroke="none" d="M' + (cx - ar) + ' ' + cy + 'A' + ar + ' ' + ar + ' 0 0 1 ' + (cx + ar) + ' ' + cy + '"/>');
+ var br = ar + 7;
+ out.push('<path id="claude-stamp-bottom" fill="none" stroke="none" d="M' + (cx - br) + ' ' + cy + 'A' + br + ' ' + br + ' 0 0 0 ' + (cx + br) + ' ' + cy + '"/>');
+ out.push('<text font-family=\'' + sans + '\' font-size="8" font-weight="800" letter-spacing="0.9" stroke="none"><textPath href="#claude-stamp-top" startOffset="50%" text-anchor="middle">APACHE SKYWALKING</textPath></text>');
+ out.push('<text font-family=\'' + sans + '\' font-size="9" font-weight="800" letter-spacing="1.4" stroke="none"><textPath href="#claude-stamp-bottom" startOffset="50%" text-anchor="middle">' + ctx.escape(issued.toUpperCase()) + '</textPath></text>');
+ out.push('<rect x="' + (cx - r - 12) + '" y="' + (cy - 15) + '" width="' + (2 * r + 24) + '" height="30" fill="#ffffff" stroke-width="2.4"/>');
+ out.push('<text x="' + cx + '" y="' + (cy + 8) + '" text-anchor="middle" font-family=\'' + sans + '\' font-size="22" font-weight="900" letter-spacing="2.8" stroke="none">VERIFIED</text>');
+ out.push('<text x="' + cx + '" y="' + (cy - 22) + '" text-anchor="middle" font-family=\'' + sans + '\' font-size="7" font-weight="800" letter-spacing="1.8" stroke="none">ISSUED</text>');
+ out.push('<text x="' + cx + '" y="' + (cy + 27) + '" text-anchor="middle" font-family=\'' + sans + '\' font-size="9" stroke="none">★</text>');
+ out.push('</g>');
+ return out.join('');
+ }
+
+ /*
+ * The verification record of a certificate, drawn from the facts of its verify link:
+ * v = { displayName, kind, apacheId, login, role, total, projects, bands, categories, issued,
+ * certId, date }. Bands give the polygon; categories are the category names published with
+ * that day's record, so the polygon is labelled as issued (unlabelled when they are unknown).
+ */
+ function renderVerification(v, ctx) {
+ var tier = TIERS[v.role] || TIERS.Contributor;
+ var sans = ctx.fonts.sans, mono = ctx.fonts.mono, e = ctx.escape;
+ var bands = v.bands.split('').map(function (b) { return parseInt(b, 11); });
+ var names = v.categories && v.categories.length === bands.length ? v.categories : null;
+ var cats = bands.map(function (b, i) { return { id: 'c' + i, name: names ? String(names[i]) : '', repos: [] }; });
+ var categories = cats.map(function (c, i) { return { id: c.id, contributions: bands[i] ? 1 : 0, band: bands[i] }; });
+ var lit = bands.filter(Boolean).length;
+ var vctx = Object.assign({}, ctx, { catalogs: cats });
+ var s = [];
+ s.push(shell(tier, '<filter id="claude-ink" x="-15%" y="-15%" width="130%" height="130%">' +
+ '<feTurbulence type="fractalNoise" baseFrequency="1.4" numOctaves="2" seed="11" result="noise"/>' +
+ '<feColorMatrix in="noise" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.5 1.45" result="speck"/>' +
+ '<feComposite in="SourceGraphic" in2="speck" operator="in"/></filter>'));
+
+ // Logo, eyebrow, recipient.
+ s.push('<image href="' + ctx.assets.skywalkingLogo + '" x="' + LEFT + '" y="60" width="169" height="40"/>');
+ s.push('<text x="' + LEFT + '" y="150" font-family=\'' + sans + '\' font-size="12.5" font-weight="700" letter-spacing="2.4" fill="' + BRAND + '">CERTIFICATE OF CONTRIBUTION · VERIFICATION</text>');
+ s.push('<text x="' + LEFT + '" y="184" font-family=\'' + sans + '\' font-size="15" fill="' + BODY + '">This certificate was issued to</text>');
+ var nameSize = ctx.fit(v.displayName, function (z) { return '800 ' + z + 'px ' + sans; }, CONTENT_W, 60, 28);
+ s.push('<text x="' + (LEFT - 2) + '" y="' + (196 + nameSize * 0.86) + '" font-family=\'' + sans + '\' font-size="' + nameSize + '" font-weight="800" letter-spacing="' + f(-nameSize * 0.02) + '" fill="' + INK + '">' + e(v.displayName) + '</text>');
+ var pillY = 280, pillText = v.role.toUpperCase();
+ var pillW = ctx.measure(pillText, '700 11.5px ' + sans) + 11.5 * 0.12 * pillText.length + 30;
+ s.push('<rect x="' + LEFT + '" y="' + pillY + '" width="' + f(pillW) + '" height="28" rx="14" fill="url(#claude-tier)"/>');
+ s.push('<text x="' + (LEFT + 15) + '" y="' + (pillY + 18.5) + '" font-family=\'' + sans + '\' font-size="11.5" font-weight="700" letter-spacing="1.4" fill="#ffffff">' + e(pillText) + '</text>');
+ var ident = v.kind === 'member' ? 'ASF ID ' + v.apacheId : 'github.com/' + v.login;
+ s.push('<text x="' + f(LEFT + pillW + 14) + '" y="' + (pillY + 18.5) + '" font-family=\'' + mono + '\' font-size="12" fill="#536478">' + e(ident) + '</text>');
+
+ var stmt = wrap('by the Apache SkyWalking PMC on ' + v.issued + '. Every fact on this record is exactly as issued: ' +
+ 'the certificate code covers them, and it is listed among the certificates skywalking.apache.org issued that day.', '15.5px ' + sans, CONTENT_W, ctx);
+ s.push(textLines(stmt, LEFT, 352, 24, 'font-family=\'' + sans + '\' font-size="15.5" fill="' + BODY + '"', ctx));
+
+ var statsY = 352 + stmt.length * 24 + 26;
+ var stats = v.total != null
+ ? [['CONTRIBUTIONS', num(v.total), ''], ['PROJECTS', String(v.projects), ''], ['CATEGORIES', String(lit), ' / ' + bands.length]]
+ : [['ROLE', v.role === 'PMC Member' ? 'PMC' : v.role, '']];
+ stats.forEach(function (st, i) {
+ var x = LEFT + i * 200;
+ s.push('<rect x="' + x + '" y="' + statsY + '" width="3" height="56" rx="1.5" fill="url(#claude-tier)"/>');
+ s.push('<text x="' + (x + 16) + '" y="' + (statsY + 14) + '" font-family=\'' + sans + '\' font-size="10.5" font-weight="700" letter-spacing="1.6" fill="' + MUTED + '">' + st[0] + '</text>');
+ s.push('<text x="' + (x + 15) + '" y="' + (statsY + 50) + '" font-family=\'' + mono + '\' font-size="34" font-weight="700" letter-spacing="-1" fill="' + INK + '">' + e(st[1]) +
+ '<tspan font-size="16" font-weight="500" fill="' + MUTED + '" letter-spacing="0">' + e(st[2]) + '</tspan></text>');
+ });
+
+ // How it was checked, beside the stamp.
+ var noteY = statsY + 96;
+ s.push('<text x="' + LEFT + '" y="' + noteY + '" font-family=\'' + sans + '\' font-size="10.5" font-weight="700" letter-spacing="1.6" fill="' + MUTED + '">HOW THIS IS VERIFIED</text>');
+ var how = wrap('The code ' + v.certId + ' is recomputed from the facts above and found in the published record ' +
+ 'skywalking.apache.org/certificate/verify/' + v.date + '.txt.', '12.5px ' + sans, CONTENT_W - 190, ctx);
+ s.push(textLines(how, LEFT, noteY + 22, 19, 'font-family=\'' + sans + '\' font-size="12.5" fill="' + BODY + '"', ctx));
+
+ s.push(footer({ issued: v.issued, certId: v.certId }, ctx));
+ s.push(stamp(LEFT + CONTENT_W - 72, Math.max(noteY + 24, statsY + 128), v.issued, ctx));
+
+ // Sky panel: the polygon as issued.
+ s.push('<text x="' + (PANEL_X + 28) + '" y="64" font-family=\'' + sans + '\' font-size="10.5" font-weight="700" letter-spacing="2" fill="#ffffff" fill-opacity=".8">CONTRIBUTION MAP</text>');
+ s.push('<text x="' + (PANEL_X + 28) + '" y="82" font-family=\'' + sans + '\' font-size="10.5" fill="#ffffff" fill-opacity=".5">As issued on ' + e(v.issued) + '</text>');
+ s.push(polygon({ categories: categories }, vctx, tier, true));
+ var captionY = CY + R + 78;
+ s.push('<text x="' + f(CX) + '" y="' + captionY + '" text-anchor="middle" font-family=\'' + mono + '\' font-size="10.5" fill="#ffffff" fill-opacity=".7">' +
+ e(lit + ' of ' + bands.length + ' categories') + '</text>');
+ [['ISSUED ON', v.issued, sans, H - 140], ['CERTIFICATE ID', v.certId, mono, H - 84]].forEach(function (m) {
+ s.push('<text x="' + f(CX) + '" y="' + m[3] + '" text-anchor="middle" font-family=\'' + sans + '\' font-size="9.5" font-weight="700" letter-spacing="1.5" fill="#ffffff" fill-opacity=".6">' + m[0] + '</text>');
+ s.push('<text x="' + f(CX) + '" y="' + (m[3] + 22) + '" text-anchor="middle" font-family=\'' + m[2] + '\' font-size="16" font-weight="700" fill="#ffffff">' + e(m[1]) + '</text>');
+ });
+
+ s.push('</svg>');
+ return s.join('');
+ }
+
+ SWCert.designs.claude = { id: 'claude', title: 'Contribution Sky Map', render: render, renderVerification: renderVerification };
+})();
diff --git a/assets/js/certificate/model.js b/assets/js/certificate/model.js
new file mode 100644
index 000000000000..90d02bbf1017
--- /dev/null
+++ b/assets/js/certificate/model.js
@@ -0,0 +1,148 @@
+/*
+ * Certificate model, shared by the browser (assets/js/certificate/page.js) and the build
+ * (scripts/certificate.js), so a certificate drawn in the browser and the list of issued codes
+ * written by the build always agree.
+ *
+ * A certificate is identified by its issue date and a code: the first 80 bits of the SHA-256
+ * of its verified facts (date, person, role, total contributions, projects, and the rank band
+ * of every category), in Crockford base32. The verify link carries the date, the code and the
+ * facts; the build publishes each day's codes in certificate/verify/<date>.txt.
+ */
+(function (root, factory) {
+ if (typeof module === 'object' && module.exports) module.exports = factory();
+ else root.SWCertModel = factory();
+})(typeof self !== 'undefined' ? self : this, function () {
+ 'use strict';
+
+ var VERSION = 'v1';
+ var ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
+ var ROLE = { 'PMC Member': 'p', 'Committer': 'c', 'Contributor': 'x' };
+ var ROLE_NAME = { p: 'PMC Member', c: 'Committer', x: 'Contributor' };
+
+ // Lookup tables without a prototype, so a login or Apache ID such as "constructor" or
+ // "__proto__" is an ordinary key.
+ function table() { return Object.create(null); }
+
+ function index(data) {
+ var logins = table(), members = table(), memberByGithub = table();
+ data.catalogs.forEach(function (c) {
+ c.repos.forEach(function (r) {
+ r.contributors.forEach(function (row) {
+ var key = row.l.toLowerCase();
+ var entry = logins[key] = logins[key] || { login: row.l, projects: [] };
+ entry.projects.push({
+ name: r.name, label: r.label, icon: r.icon || null, archived: !!r.archived, catalog: c.id,
+ contributions: row.n, rank: row.r, of: r.of
+ });
+ });
+ });
+ });
+ [['pmc', 'PMC Member'], ['committer', 'Committer']].forEach(function (g) {
+ (data.members[g[0]] || []).forEach(function (m) {
+ var member = { name: m.name, apacheId: m.apacheId, github: m.github || null, role: g[1] };
+ members[m.apacheId.toLowerCase()] = member;
+ if (m.github) memberByGithub[m.github.toLowerCase()] = member;
+ });
+ });
+ // Every listed person's total in each category, highest first, for category ranks.
+ var categoryTotals = table();
+ data.catalogs.forEach(function (c) { categoryTotals[c.id] = []; });
+ Object.keys(logins).forEach(function (k) {
+ var sums = table();
+ logins[k].projects.forEach(function (p) { sums[p.catalog] = (sums[p.catalog] || 0) + p.contributions; });
+ Object.keys(sums).forEach(function (id) { categoryTotals[id].push(sums[id]); });
+ });
+ Object.keys(categoryTotals).forEach(function (id) {
+ categoryTotals[id].sort(function (a, b) { return b - a; });
+ });
+ return { logins: logins, members: members, memberByGithub: memberByGithub, categoryTotals: categoryTotals };
+ }
+
+ // Competition ranking: 1 + the number of people with strictly more, so ties share a rank.
+ function rankIn(descending, value) {
+ var lo = 0, hi = descending.length;
+ while (lo < hi) {
+ var mid = (lo + hi) >> 1;
+ if (descending[mid] > value) lo = mid + 1; else hi = mid;
+ }
+ return lo + 1;
+ }
+
+ // Find who a query refers to. Members win, so a committer's GitHub link still yields their role.
+ function resolve(idx, query) {
+ var member = query.member ? idx.members[query.member.toLowerCase()] : null;
+ var login = query.contributor || null;
+ if (!member && login) member = idx.memberByGithub[login.toLowerCase()] || null;
+ if (!member && !login && query.q) {
+ member = idx.members[query.q.toLowerCase()] || idx.memberByGithub[query.q.toLowerCase()] || null;
+ login = member ? null : query.q;
+ }
+ if (member) login = member.github;
+ var stats = login ? idx.logins[login.toLowerCase()] || null : null;
+ if (!member && !stats) return null;
+ return { member: member, stats: stats };
+ }
+
+ // A person's standing in every category, in catalog order. The band (0-10) places the
+ // category's vertex on the contribution polygon: rank in 10% steps, 10 for the top 10%.
+ function categories(data, idx, found) {
+ var sums = table();
+ (found.stats ? found.stats.projects : []).forEach(function (p) {
+ sums[p.catalog] = (sums[p.catalog] || 0) + p.contributions;
+ });
+ return data.catalogs.map(function (c) {
+ var v = sums[c.id] || 0, all = idx.categoryTotals[c.id];
+ var rank = v ? rankIn(all, v) : null;
+ return {
+ id: c.id, name: c.name, contributions: v, rank: rank, of: all.length,
+ band: v ? 10 - Math.floor((rank - 1) * 10 / all.length) : 0
+ };
+ });
+ }
+
+ // The facts a certificate's code covers.
+ function facts(data, idx, found) {
+ var m = found.member, projects = found.stats ? found.stats.projects : [];
+ return {
+ key: m ? 'member:' + m.apacheId.toLowerCase() : 'contributor:' + found.stats.login.toLowerCase(),
+ role: m ? ROLE[m.role] : 'x',
+ total: projects.length ? projects.reduce(function (s, p) { return s + p.contributions; }, 0) : null,
+ projects: projects.length,
+ bands: categories(data, idx, found).map(function (c) { return c.band.toString(11); }).join('')
+ };
+ }
+
+ function canonical(date, f) {
+ return [VERSION, date, f.key, f.role, f.total == null ? '' : f.total, f.projects, f.bands].join('|');
+ }
+
+ // The first 80 bits of a SHA-256 digest (bytes) as 16 Crockford base32 characters.
+ function encode(bytes) {
+ var out = '', bits = 0, value = 0;
+ for (var i = 0; i < 10; i++) {
+ value = (value << 8) | bytes[i];
+ bits += 8;
+ while (bits >= 5) {
+ out += ALPHABET[(value >>> (bits - 5)) & 31];
+ bits -= 5;
+ }
+ value &= (1 << bits) - 1;
+ }
+ return out;
+ }
+
+ // Everyone the site lists on a given day: members, then contributors who are not members.
+ function everyone(idx) {
+ var people = Object.keys(idx.members).map(function (id) { return { member: id }; });
+ Object.keys(idx.logins).forEach(function (login) {
+ if (!idx.memberByGithub[login]) people.push({ contributor: login });
+ });
+ return people;
+ }
+
+ return {
+ VERSION: VERSION, ROLE: ROLE, ROLE_NAME: ROLE_NAME,
+ index: index, rankIn: rankIn, resolve: resolve, categories: categories,
+ facts: facts, canonical: canonical, encode: encode, everyone: everyone
+ };
+});
diff --git a/assets/js/certificate/page.js b/assets/js/certificate/page.js
new file mode 100644
index 000000000000..96e156c8d0a4
--- /dev/null
+++ b/assets/js/certificate/page.js
@@ -0,0 +1,293 @@
+/*
+ * /certificate page controller.
+ *
+ * Issuing: ?contributor=<GitHub login> or ?member=<Apache ID> draws a certificate from today's
+ * data (assets/certificate/people.json, see scripts/certificate.js). Its ID is the data date plus
+ * a code covering its facts (SWCertModel), and its QR code links back here with both.
+ *
+ * Verifying: a link with &id=<yyyymmdd>-<code> carries the certificate's facts. The page
+ * recomputes the code from them, checks it against the ID, and looks it up in that day's list
+ * of issued codes (certificate/verify/<yyyy-mm-dd>.txt), which the site keeps for good.
+ */
+(function () {
+ 'use strict';
+
+ var app = document.getElementById('cert-app');
+ if (!app) return;
+ var M = window.SWCertModel;
+ var $ = function (sel) { return app.querySelector(sel); };
+ var SITE = (app.dataset.base || 'https://skywalking.apache.org/').replace(/\/?$/, '/');
+ var ID = /^(\d{4})(\d{2})(\d{2})-([0-9A-HJKMNP-TV-Z]{16})$/;
+ var MONTHS = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'];
+
+ function siteDate(iso) {
+ var d = new Date(iso + 'T00:00:00'), day = d.getDate();
+ var suffix = (day % 100 >= 11 && day % 100 <= 13) ? 'th' : ({ 1: 'st', 2: 'nd', 3: 'rd' }[day % 10] || 'th');
+ return MONTHS[d.getMonth()] + ' ' + day + suffix + ', ' + d.getFullYear();
+ }
+
+ function codeFor(date, facts) {
+ return crypto.subtle.digest('SHA-256', new TextEncoder().encode(M.canonical(date, facts)))
+ .then(function (buf) { return M.encode(new Uint8Array(buf)); });
+ }
+
+ function groupCode(code) { return code.match(/.{4}/g).join('-'); }
+
+ function esc(s) {
+ return String(s).replace(/[&<>"']/g, function (c) {
+ return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
+ });
+ }
+
+ function plural(n, word) { return n.toLocaleString('en-US') + ' ' + word + (n === 1 ? '' : 's'); }
+ function asRole(role) { return role === 'PMC Member' ? 'a PMC member' : 'a ' + role.toLowerCase(); }
+ function splitName(name) {
+ var t = /^(.*?)\s*\((.*)\)\s*$/.exec(name);
+ return t ? { name: t[1], note: t[2] } : { name: name, note: null };
+ }
+ var TICK = '<span class="tick" aria-hidden="true">✓</span>';
+
+ var state = { data: null, idx: null, ctx: null, person: null, svg: null };
+
+ // Every lookup gets a generation; an answer that arrives after a newer lookup started is
+ // dropped, so a slow verification can never overwrite the result of a later search.
+ var generation = 0;
+ function stale(token) { return token !== generation; }
+
+ function setStatus(kind, html) {
+ var el = $('.cert-status');
+ el.className = 'cert-status ' + kind;
+ el.innerHTML = html;
+ }
+
+ function fail(html, token) {
+ if (stale(token)) return;
+ state.person = null;
+ $('.cert-result').hidden = true;
+ $('.cert-verify').hidden = true;
+ setStatus('missing', html);
+ }
+
+ // ---- Issuing ------------------------------------------------------------------------------
+
+ function buildPerson(found) {
+ var data = state.data, m = found.member, st = found.stats;
+ var projects = st ? st.projects.slice().sort(function (a, b) {
+ return b.contributions - a.contributions || a.rank - b.rank;
+ }) : [];
+ var named = m ? splitName(m.name) : null;
+ var facts = M.facts(data, state.idx, found);
+ var date = data.generated;
+ return codeFor(date, facts).then(function (code) {
+ var id = date.replace(/-/g, '') + '-' + code;
+ var query = (m ? 'member=' + encodeURIComponent(m.apacheId) : 'contributor=' + encodeURIComponent(st.login)) +
+ '&id=' + id + '&r=' + facts.role + (facts.total == null ? '' : '&t=' + facts.total) +
+ '&n=' + facts.projects + '&b=' + facts.bands;
+ return {
+ kind: m ? 'member' : 'contributor',
+ displayName: m ? named.name : '@' + st.login,
+ login: st ? st.login : (m && m.github) || null, apacheId: m ? m.apacheId : null,
+ role: m ? m.role : 'Contributor', roleNote: named ? named.note : null,
+ total: facts.total, projectCount: facts.projects, top: projects.slice(0, 2), projects: projects,
+ categories: M.categories(data, state.idx, found),
+ verifyUrl: SITE + 'certificate/?' + query,
+ certId: groupCode(code),
+ issued: siteDate(date)
+ };
+ });
+ }
+
+ function issue(p, token) {
+ var found = M.resolve(state.idx, p);
+ if (!found) {
+ return fail('<strong>Not found.</strong> No PMC member, committer or contributor <code>' + esc(p.member || p.contributor || p.q) +
+ '</code> is listed on skywalking.apache.org. Contributors are matched by GitHub login; anonymous commits ' +
+ 'and bots are not listed.', token);
+ }
+ setStatus('idle', 'Preparing the certificate…');
+ return buildPerson(found).then(function (person) {
+ if (stale(token)) return;
+ state.person = person;
+ var detail = person.total != null
+ ? plural(person.total, 'contribution') + ' across ' + plural(person.projectCount, 'project')
+ : 'no public commit statistics on file';
+ // Issuing is not verification: that only happens when a certificate's verify link is opened.
+ setStatus('issued', '<span><strong>Certificate generated</strong> for ' + esc(person.displayName) + ', listed on skywalking.apache.org as ' +
+ asRole(person.role) + (person.roleNote ? ' (' + esc(person.roleNote) + ')' : '') + ', with ' + detail +
+ ' as of ' + esc(person.issued) + '. Certificate ID <code>' + esc(person.certId) + '</code>; anyone can verify it from its QR code or verify link.</span>');
+ $('.cert-verify').hidden = true;
+ $('.cert-result').hidden = false;
+ state.svg = SWCert.render('claude', person, state.ctx);
+ $('.cert-result .cert-canvas').innerHTML = state.svg;
+ }).catch(function (err) {
+ fail('Could not draw the certificate: ' + esc(err.message), token);
+ });
+ }
+
+ // ---- Verifying ----------------------------------------------------------------------------
+
+ function claimedFacts(p) {
+ var key = p.member ? 'member:' + p.member.toLowerCase() : (p.contributor ? 'contributor:' + p.contributor.toLowerCase() : null);
+ if (!key || (p.member && p.contributor)) return null;
+ if (!/^[pcx]$/.test(p.r || '') || !/^\d*$/.test(p.t || '') || !/^\d+$/.test(p.n || '') || !/^[0-9a]+$/.test(p.b || '')) return null;
+ return { key: key, role: p.r, total: p.t ? Number(p.t) : null, projects: Number(p.n), bands: p.b };
+ }
+
+ // A day's record: '#' header lines (the day's categories, in polygon order) and one code per
+ // line. Lines may come in any order; differing headers leave the categories unknown.
+ function issuedRecord(date) {
+ return fetch(app.dataset.verify + date + '.txt').then(function (r) {
+ if (!r.ok) return null;
+ return r.text().then(function (text) {
+ var codes = [], headers = [];
+ text.split('\n').forEach(function (line) {
+ line = line.trim();
+ if (line.charAt(0) === '#') headers.push(line);
+ else if (line) codes.push(line);
+ });
+ var categories = null;
+ if (headers.length && headers.every(function (h) { return h === headers[0]; })) {
+ try { categories = JSON.parse(headers[0].slice(1)).categories || null; } catch (e) { categories = null; }
+ }
+ return { codes: codes, categories: categories };
+ });
+ });
+ }
+
+ function verify(p, token) {
+ var parts = ID.exec(p.id || '');
+ var facts = claimedFacts(p);
+ if (!parts || !facts) return fail('<strong>Not verifiable.</strong> This link is not a complete certificate verification link.', token);
+ var date = parts[1] + '-' + parts[2] + '-' + parts[3], code = parts[4], shown = groupCode(code);
+ setStatus('idle', 'Checking certificate <code>' + esc(shown) + '</code>…');
+ return codeFor(date, facts).then(function (expected) {
+ if (stale(token)) return;
+ if (expected !== code) {
+ return fail('<strong>Not authentic.</strong> The facts in this link do not match certificate <code>' + esc(shown) +
+ '</code>: they have been changed since it was issued.', token);
+ }
+ return issuedRecord(date).then(function (record) {
+ if (stale(token)) return;
+ if (!record) return fail('<strong>Not verifiable.</strong> No certificate records are published for ' + esc(siteDate(date)) + '.', token);
+ if (record.codes.indexOf(code) < 0) {
+ return fail('<strong>Not authentic.</strong> Certificate <code>' + esc(shown) + '</code> was not issued on ' + esc(siteDate(date)) + '.', token);
+ }
+ showVerified(p, facts, date, shown, record.categories);
+ });
+ }).catch(function (err) {
+ fail('<strong>Not verifiable right now.</strong> The certificate records could not be loaded (' + esc(err.message) +
+ '). Try again later.', token);
+ });
+ }
+
+ function showVerified(p, facts, date, shown, categories) {
+ var now = M.resolve(state.idx, p.member ? { member: p.member } : { contributor: p.contributor });
+ var role = M.ROLE_NAME[facts.role];
+ var name = now && now.member ? splitName(now.member.name).name : (p.member || '@' + p.contributor);
+ setStatus('verified', TICK + '<span><strong>Authentic.</strong> Certificate <code>' + esc(shown) + '</code> was issued to ' +
+ esc(name) + ' on ' + esc(siteDate(date)) + ', and every fact below matches it.</span>');
+ $('.cert-verify-canvas').innerHTML = SWCert.designs.claude.renderVerification({
+ displayName: name, kind: p.member ? 'member' : 'contributor', apacheId: p.member, login: p.contributor,
+ role: role, total: facts.total, projects: facts.projects, bands: facts.bands,
+ categories: categories, issued: siteDate(date), certId: shown, date: date
+ }, state.ctx);
+ $('.cert-verify-today').innerHTML = todayLine(now, name, role, facts) +
+ ' <a href="' + esc(location.pathname + '?' + (p.member ? 'member=' + encodeURIComponent(p.member) : 'contributor=' + encodeURIComponent(p.contributor))) +
+ '">See today\'s certificate →</a>';
+ $('.cert-result').hidden = true;
+ $('.cert-verify').hidden = false;
+ }
+
+ function todayLine(now, name, role, facts) {
+ if (!now) return 'Today ' + esc(name) + ' is no longer listed on skywalking.apache.org.';
+ var roleNow = now.member ? now.member.role : 'Contributor';
+ var line = 'Today ' + esc(name) + ' is ' + (roleNow === role ? 'still listed as ' + asRole(roleNow)
+ : 'listed as ' + asRole(roleNow) + ' (' + role.toLowerCase() + ' when issued)');
+ var total = now.stats ? now.stats.projects.reduce(function (s, x) { return s + x.contributions; }, 0) : null;
+ if (facts.total == null || total == null) return line + '.';
+ if (total === facts.total) return line + ', with the same ' + plural(total, 'contribution') + '.';
+ if (total > facts.total) return line + ', with ' + plural(total, 'contribution') + ' (' + facts.total.toLocaleString('en-US') + ' when issued).';
+ return line + ', with ' + plural(total, 'contribution') + ', fewer than the ' + facts.total.toLocaleString('en-US') +
+ ' when issued. That happens when commit history is rewritten or a repository leaves the site.';
+ }
+
+ // ---- Page ---------------------------------------------------------------------------------
+
+ function params() {
+ var q = new URLSearchParams(location.search), p = {};
+ ['contributor', 'member', 'q', 'id', 'r', 't', 'n', 'b'].forEach(function (k) { p[k] = q.get(k); });
+ return p;
+ }
+
+ function show(p) {
+ var token = ++generation;
+ state.person = null;
+ state.svg = null;
+ $('.cert-result').hidden = true;
+ $('.cert-verify').hidden = true;
+ if (p.id) return verify(p, token);
+ if (!(p.member || p.contributor || p.q)) {
+ setStatus('idle', 'Enter a GitHub login or an Apache ID to generate a certificate.');
+ return;
+ }
+ return issue(p, token);
+ }
+
+ function navigate(p) {
+ var q = new URLSearchParams();
+ if (p.member) q.set('member', p.member);
+ else if (p.contributor) q.set('contributor', p.contributor);
+ else if (p.q) q.set('q', p.q);
+ history.pushState(null, '', location.pathname + (q.toString() ? '?' + q : ''));
+ show(p);
+ }
+
+ $('.cert-png').addEventListener('click', function () {
+ if (state.svg) SWCert.downloadPNG(state.svg, state.person, 2);
+ });
+ $('.cert-print').addEventListener('click', function () { window.print(); });
+ $('.cert-copy').addEventListener('click', function (ev) {
+ var btn = ev.currentTarget;
+ navigator.clipboard.writeText(state.person.verifyUrl).then(function () {
+ btn.classList.add('copied');
+ setTimeout(function () { btn.classList.remove('copied'); }, 1600);
+ });
+ });
+
+ var form = document.querySelector('.cert-search');
+ if (form) {
+ form.addEventListener('submit', function (ev) {
+ ev.preventDefault();
+ var v = form.q.value.trim().replace(/^@/, '');
+ if (v) navigate({ q: v });
+ });
+ }
+ window.addEventListener('popstate', function () { show(params()); });
+
+ setStatus('idle', 'Loading contribution data…');
+ fetch(app.dataset.people).then(function (r) {
+ if (!r.ok) throw new Error('certificate data is not published');
+ return r.json();
+ }).then(function (data) {
+ state.data = data;
+ state.idx = M.index(data);
+ return SWCert.buildContext({
+ files: {
+ skywalkingLogo: app.dataset.logo,
+ skywalkingLogoWhite: app.dataset.logoWhite,
+ asfLogo: app.dataset.asfLogo,
+ asfOakleaf: app.dataset.asfLeaf
+ },
+ iconBase: app.dataset.icons,
+ catalogs: data.catalogs,
+ totalPeople: data.totalPeople
+ });
+ }).then(function (ctx) {
+ state.ctx = ctx;
+ var p = params();
+ if (form && !p.id && (p.member || p.contributor || p.q)) form.q.value = p.member || p.contributor || p.q;
+ show(p);
+ }).catch(function (err) {
+ setStatus('missing', 'Could not load contribution data: ' + esc(err.message));
+ });
+})();
diff --git a/assets/js/certificate/runtime.js b/assets/js/certificate/runtime.js
new file mode 100644
index 000000000000..ba3096a7c750
--- /dev/null
+++ b/assets/js/certificate/runtime.js
@@ -0,0 +1,154 @@
+/*
+ * SkyWalking certificate runtime — shared by every certificate design.
+ *
+ * A design registers itself as SWCert.designs[id] = { id, title, render(person, ctx) } and
+ * returns a complete, self-contained SVG string (viewBox 0 0 1123 794, A4 landscape).
+ * The runtime supplies ctx: inlined assets (data: URIs, so the exported PNG has no external
+ * references), a QR encoder, text measurement for auto-fit, and escaping.
+ */
+(function () {
+ 'use strict';
+
+ var SWCert = window.SWCert = window.SWCert || {};
+ SWCert.designs = SWCert.designs || {};
+ SWCert.WIDTH = 1123;
+ SWCert.HEIGHT = 794;
+
+ var FONTS = {
+ sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ mono: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace'
+ };
+
+ function toDataURI(url) {
+ return fetch(url).then(function (r) {
+ if (!r.ok) throw new Error(url + ' ' + r.status);
+ return r.blob();
+ }).then(function (blob) {
+ return new Promise(function (resolve) {
+ var fr = new FileReader();
+ fr.onload = function () { resolve(fr.result); };
+ fr.readAsDataURL(blob);
+ });
+ });
+ }
+
+ function escapeXml(s) {
+ return String(s == null ? '' : s)
+ .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
+ .replace(/"/g, '"').replace(/'/g, ''');
+ }
+
+ var measureCtx = null;
+ function measure(text, font) {
+ measureCtx = measureCtx || document.createElement('canvas').getContext('2d');
+ measureCtx.font = font;
+ return measureCtx.measureText(String(text)).width;
+ }
+
+ // Largest font size in [min, max] at which text fits maxWidth. fontFor(size) returns a CSS font.
+ function fit(text, fontFor, maxWidth, max, min) {
+ for (var size = max; size > min; size -= 1) {
+ if (measure(text, fontFor(size)) <= maxWidth) return size;
+ }
+ return min;
+ }
+
+ // QR modules for text. path(x, y, size) returns one SVG path covering every dark module,
+ // drawn inside a size×size square (no quiet zone; callers leave >= 4 modules of margin).
+ function qr(text) {
+ var q = window.qrcode(0, 'M');
+ q.addData(text);
+ q.make();
+ var n = q.getModuleCount();
+ return {
+ size: n,
+ isDark: function (r, c) { return q.isDark(r, c); },
+ path: function (x, y, size) {
+ // One subpath per horizontal run of dark modules keeps the SVG small.
+ var cell = size / n, d = [];
+ for (var r = 0; r < n; r++) {
+ for (var c = 0; c < n; c++) {
+ if (!q.isDark(r, c)) continue;
+ var run = 1;
+ while (c + run < n && q.isDark(r, c + run)) run++;
+ var w = run * cell;
+ d.push('M' + (x + c * cell).toFixed(2) + ' ' + (y + r * cell).toFixed(2) +
+ 'h' + w.toFixed(2) + 'v' + cell.toFixed(2) + 'h-' + w.toFixed(2) + 'z');
+ c += run - 1;
+ }
+ }
+ return d.join('');
+ }
+ };
+ }
+
+ SWCert.fonts = FONTS;
+
+ /*
+ * opts: { files: { skywalkingLogo, skywalkingLogoWhite, asfLogo, asfOakleaf } (URLs),
+ * iconBase: URL prefix of the per-project icons (<icon>.svg), catalogs, totalPeople }
+ * Resolves to the ctx object passed to every design's render().
+ */
+ SWCert.buildContext = function (opts) {
+ var files = opts.files;
+ var iconBase = opts.iconBase.replace(/\/?$/, '/');
+ var icons = {};
+ (opts.catalogs || []).forEach(function (c) {
+ c.repos.forEach(function (r) { if (r.icon) icons[r.icon] = true; });
+ });
+ var assets = { projectIcons: {} };
+ var jobs = Object.keys(files).map(function (k) {
+ return toDataURI(files[k]).then(function (u) { assets[k] = u; });
+ }).concat(Object.keys(icons).map(function (k) {
+ return toDataURI(iconBase + k + '.svg')
+ .then(function (u) { assets.projectIcons[k] = u; })
+ .catch(function () { /* missing icon: designs fall back to a glyph */ });
+ }));
+ return Promise.all(jobs).then(function () {
+ return {
+ width: SWCert.WIDTH, height: SWCert.HEIGHT,
+ fonts: FONTS,
+ assets: assets,
+ catalogs: opts.catalogs || [],
+ totalPeople: opts.totalPeople || 0,
+ qr: qr, measure: measure, fit: fit, escape: escapeXml
+ };
+ });
+ };
+
+ SWCert.render = function (designId, person, ctx) {
+ var design = SWCert.designs[designId];
+ if (!design) throw new Error('Unknown certificate design: ' + designId);
+ return design.render(person, ctx);
+ };
+
+ function slug(person) {
+ return String(person.login || person.apacheId || 'certificate').replace(/[^A-Za-z0-9_-]/g, '');
+ }
+
+ // Rasterise an SVG string to PNG at the given scale and download it.
+ SWCert.downloadPNG = function (svg, person, scale) {
+ scale = scale || 2;
+ return new Promise(function (resolve, reject) {
+ var img = new Image();
+ img.onload = function () {
+ var canvas = document.createElement('canvas');
+ canvas.width = SWCert.WIDTH * scale;
+ canvas.height = SWCert.HEIGHT * scale;
+ var g = canvas.getContext('2d');
+ g.drawImage(img, 0, 0, canvas.width, canvas.height);
+ canvas.toBlob(function (blob) {
+ var a = document.createElement('a');
+ a.href = URL.createObjectURL(blob);
+ a.download = 'apache-skywalking-certificate-' + slug(person) + '.png';
+ document.body.appendChild(a);
+ a.click();
+ setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
+ resolve({ width: canvas.width, height: canvas.height, bytes: blob.size });
+ }, 'image/png');
+ };
+ img.onerror = function () { reject(new Error('SVG failed to rasterise')); };
+ img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
+ });
+ };
+})();
diff --git a/assets/scss/_custom_certificate.scss b/assets/scss/_custom_certificate.scss
new file mode 100644
index 000000000000..5a74d77a5edd
--- /dev/null
+++ b/assets/scss/_custom_certificate.scss
@@ -0,0 +1,105 @@
+// =====================================================================
+// /certificate page — light theme, consistent with team/contributors.
+// The certificate itself is an inline SVG drawn by static/js/certificate/.
+// (uses $ink/$body/$muted/$line/$brand/$brand-grad from _custom_home.scss)
+// =====================================================================
+.td-certificate {
+ background: #fff;
+
+ .eyebrow {
+ display: inline-block; font-size: 12px; font-weight: 700;
+ letter-spacing: .14em; text-transform: uppercase; color: $brand; margin-bottom: 12px;
+ }
+
+ .cert-hero {
+ padding: calc(4rem + 30px) 0 28px;
+ background:
+ radial-gradient(800px 380px at 12% 0%, rgba(55,136,208,.10), transparent 60%),
+ radial-gradient(640px 340px at 100% 0%, rgba(232,78,161,.06), transparent 55%),
+ linear-gradient(180deg, #f7faff 0%, #fff 70%);
+ border-bottom: 1px solid $line;
+ h1 { font-size: 40px; font-weight: 800; color: $ink; letter-spacing: -.8px; margin: 0 0 12px; border: 0; padding: 0; }
+ p { font-size: 16px; color: $body; line-height: 1.6; margin: 0; max-width: 720px; }
+ }
+
+ .cert-search {
+ display: flex; gap: 8px; margin-top: 22px; max-width: 520px;
+ input {
+ flex: 1; min-width: 0; min-height: 44px; padding: 10px 14px;
+ border: 1px solid #c4d3e4; border-radius: 8px; background: #fff; color: $ink;
+ font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 14px;
+ &:focus { outline: none; border-color: $brand; box-shadow: 0 0 0 3px rgba(55,136,208,.15); }
+ }
+ button {
+ min-height: 44px; padding: 0 18px; border: 0; border-radius: 8px; cursor: pointer;
+ background: $brand-grad; color: #fff; font-weight: 700; font-size: 14px; white-space: nowrap;
+ &:hover { filter: brightness(1.05); }
+ }
+ }
+
+ .cert-wrap { max-width: 1200px; padding-top: 28px; padding-bottom: 64px; }
+
+ .cert-status {
+ display: flex; align-items: flex-start; gap: 10px;
+ padding: 12px 16px; border-radius: 10px; font-size: 14.5px; line-height: 1.55;
+ border: 1px solid $line; background: #f7faff; color: $body;
+ code { font-size: .95em; color: $ink; background: transparent; }
+ &.issued { border-color: #d6e4f5; background: #eef5fd; color: #2c4a6b; }
+ &.verified { border-color: #bfe3cf; background: #f0faf4; color: #24543a; }
+ &.missing { border-color: #f1d0c4; background: #fdf5f2; color: #7a3a22; }
+ .tick {
+ flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
+ width: 20px; height: 20px; margin-top: 1px; border-radius: 50%;
+ background: #2f9e62; color: #fff; font-size: 12px; font-weight: 800;
+ }
+ }
+
+ // Verification record: drawn like the certificate, with the VERIFIED stamp.
+ .cert-verify { margin-top: 20px; }
+ .cert-verify-today { margin: 16px 2px 0; font-size: 14.5px; color: $body; line-height: 1.6; a { color: $brand; font-weight: 600; white-space: nowrap; } }
+
+ .cert-toolbar {
+ display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
+ margin: 20px 0 14px;
+ }
+ .cert-actions {
+ display: inline-flex; flex-wrap: wrap; gap: 8px;
+ button {
+ appearance: none; min-height: 38px; padding: 0 14px; border-radius: 8px; cursor: pointer;
+ border: 1px solid #c4d3e4; background: #fff; color: $ink; font-size: 13.5px; font-weight: 600;
+ &:hover { border-color: $brand; color: $brand; }
+ &.cert-png { background: $brand-grad; border-color: transparent; color: #fff; &:hover { color: #fff; filter: brightness(1.05); } }
+ }
+ .cert-copy .done { display: none; }
+ .cert-copy.copied { .label { display: none; } .done { display: inline; color: #2f9e62; } }
+ }
+
+ .cert-stage {
+ padding: clamp(10px, 2.4vw, 28px); border: 1px solid $line; border-radius: 16px;
+ background: repeating-linear-gradient(45deg, #f7f9fc, #f7f9fc 10px, #f3f6fa 10px, #f3f6fa 20px);
+ }
+ .cert-canvas svg {
+ display: block; width: 100%; height: auto;
+ border-radius: 10px; box-shadow: 0 24px 48px -28px rgba(17,24,39,.45);
+ }
+
+ .cert-note { margin: 18px 2px 0; font-size: 13px; color: $muted; line-height: 1.6; a { color: $brand; } }
+
+ @media (max-width: 575px) {
+ .cert-hero h1 { font-size: 30px; }
+ .cert-search { flex-direction: column; }
+ }
+}
+
+// Print / Save as PDF: the certificate alone, one A4 landscape page. The @page size is set
+// in layouts/certificate/baseof.html so it applies to this page only.
+@media print {
+ body.td-certificate {
+ > :not(.td-outer) { display: none !important; } // header, widgets injected into <body> (Ask AI)
+ header, footer, .cert-hero, .cert-status, .cert-toolbar, .cert-note, .cert-verify-today, .td-sidebar, #popup { display: none !important; }
+ .cert-verify { margin: 0; }
+ .td-outer, .td-main, .cert-wrap { padding: 0 !important; margin: 0 !important; max-width: none !important; }
+ .cert-stage { padding: 0; border: 0; background: none; }
+ .cert-canvas svg { width: 297mm; height: 210mm; border-radius: 0; box-shadow: none; }
+ }
+}
diff --git a/assets/scss/_custom_contributors.scss b/assets/scss/_custom_contributors.scss
index 6a2bc456ea92..45475aa145d3 100644
--- a/assets/scss/_custom_contributors.scss
+++ b/assets/scss/_custom_contributors.scss
@@ -29,6 +29,12 @@
.icon-github { font-size: .9em; } }
}
p { font-size: 15px; color: $body; line-height: 1.6; margin: 0; a { color: $brand; } }
+ .cert-cta {
+ display: inline-flex; align-items: center; margin-top: 16px; padding: 9px 16px; border-radius: 8px;
+ background: $brand-grad; color: #fff; font-size: 14px; font-weight: 700; text-decoration: none;
+ &:hover { filter: brightness(1.05); color: #fff; }
+ &[hidden] { display: none; }
+ }
}
.contrib-wrap { max-width: 980px; padding-top: 28px; padding-bottom: 64px; }
diff --git a/assets/scss/_custom_team.scss b/assets/scss/_custom_team.scss
index 8983a30f78d0..df8c6c6c04d9 100644
--- a/assets/scss/_custom_team.scss
+++ b/assets/scss/_custom_team.scss
@@ -23,6 +23,29 @@ $link-hook: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' vi
p { font-size: 16px; color: $body; line-height: 1.6; margin: 0; max-width: 680px; a { color: $brand; font-weight: 600; } }
}
+ // The hero text on the left, the certificate badge on the right.
+ .team-hero-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 32px; }
+ .team-hero-text { min-width: 0; }
+ .cert-badge {
+ flex: 0 0 auto; display: flex; align-items: center; gap: 14px; margin-top: 18px;
+ padding: 14px 18px 14px 14px; border: 1px solid #d6e4f5; border-radius: 16px;
+ background: linear-gradient(180deg, #fff 0%, #f5f9fe 100%); color: $ink; text-decoration: none;
+ box-shadow: 0 18px 36px -26px rgba(17,24,39,.45);
+ transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
+ &:hover { transform: translateY(-2px); border-color: #b9d3ee; box-shadow: 0 22px 40px -24px rgba(17,24,39,.5); color: $ink; text-decoration: none; }
+ .cert-badge-seal { flex: 0 0 auto; display: block; filter: drop-shadow(0 6px 10px rgba(44,108,176,.28)); }
+ .cert-badge-text { display: flex; flex-direction: column; gap: 2px; }
+ .cert-badge-kicker { font-size: 10.5px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: $brand; }
+ strong { font-size: 17px; font-weight: 800; letter-spacing: -.2px; color: $ink; }
+ .cert-badge-sub { font-size: 12.5px; color: $body; }
+ .cert-badge-arrow { margin-left: 4px; font-size: 18px; font-weight: 700; color: $brand; transition: transform .18s ease; }
+ &:hover .cert-badge-arrow { transform: translateX(3px); }
+ }
+ @media (max-width: 991px) {
+ .team-hero-row { flex-direction: column; gap: 0; }
+ .cert-badge { margin-top: 20px; }
+ }
+
.team-wrap { max-width: 1200px; padding-top: 32px; padding-bottom: 64px; }
// --- Members section (data-driven from data/committee.yml) ---
diff --git a/content/certificate/_index.md b/content/certificate/_index.md
new file mode 100644
index 000000000000..0ef61b03ea86
--- /dev/null
+++ b/content/certificate/_index.md
@@ -0,0 +1,7 @@
+---
+title: Certificate of Contribution
+linkTitle: Certificate
+description: Get an Apache SkyWalking Certificate of Contribution, drawn from the project's public contribution data and verifiable by QR code.
+---
+
+{{< certificate >}}
diff --git a/data/repositories.yml b/data/repositories.yml
new file mode 100644
index 000000000000..6b2196b19776
--- /dev/null
+++ b/data/repositories.yml
@@ -0,0 +1,92 @@
+# Every repository whose contributors this site counts, grouped into the contribution
+# categories of the Certificate of Contribution (/certificate/).
+#
+# Each repository listed in data/projects.yml, the Showcase (data/get-started.yml) and the
+# Website (package.json) must appear here exactly once, as owner/repo. A new repository is
+# added to its category here in the same change that adds it to the site;
+# scripts/project-config.js rejects the configuration otherwise, which fails the build.
+#
+# Category order is the order of the vertices of the certificate's contribution polygon,
+# clockwise from the top. Archived and retired repositories stay in their category.
+categories:
+ - id: protocol
+ name: Protocol
+ repos:
+ - apache/skywalking-data-collect-protocol
+ - apache/skywalking-query-protocol
+ - apache/skywalking-goapi
+
+ - id: main
+ name: Main
+ repos:
+ - apache/skywalking
+ - apache/skywalking-graalvm-distro
+ - apache/skywalking-ai-sessionizer
+
+ - id: agent
+ name: Agent
+ repos:
+ - apache/skywalking-java
+ - apache/skywalking-python
+ - apache/skywalking-nodejs
+ - apache/skywalking-go
+ - apache/skywalking-rust
+ - apache/skywalking-php
+ - apache/skywalking-client-js
+ - apache/skywalking-ruby
+ - apache/skywalking-nginx-lua
+ - apache/skywalking-kong
+ - apache/skywalking-satellite
+ - apache/skywalking-kubernetes-event-exporter
+ - apache/skywalking-rover
+
+ - id: database
+ name: Database
+ repos:
+ - apache/skywalking-banyandb
+ - apache/skywalking-banyandb-client-proto
+ - apache/skywalking-banyandb-helm
+ - apache/skywalking-banyandb-java-client
+
+ - id: website
+ name: Blogs & Website
+ repos:
+ - apache/skywalking-website
+
+ - id: tools
+ name: Tools
+ repos:
+ - apache/skywalking-eyes
+ - apache/skywalking-infra-e2e
+ - apache/skywalking-helm
+ - apache/skywalking-swck
+ - apache/skywalking-cli
+ - apache/skywalking-mcp
+ - apache/skywalking-agent-test-tool
+ - apache/skywalking-showcase
+ - apache/skywalking-docker
+ - apache/skywalking-oal-tool
+
+ - id: visualization
+ name: Visualization
+ repos:
+ - apache/skywalking-horizon-ui
+ - apache/skywalking-booster-ui
+ - apache/skywalking-rocketbot-ui
+ - apache/skywalking-ui
+ - apache/skywalking-grafana-plugins
+
+ - id: community
+ name: Community Projects
+ repos:
+ - SkyAPM/SkyAPM-dotnet
+ - SkyAPM/cpp2sky
+ - SkyAPM/java-plugin-extensions
+ - SkyAPM/uranus
+ - SkyAPM/document-cn-translation-of-skywalking
+ - SkyAPM/transporter-plugin-for-skywalking
+ - SkyAPM/go2sky
+ - SkyAPM/go2sky-plugins
+ - SkyAPM/SkyAPM-php-sdk
+ - SkyAPM/SkyAPM-nodejs
+ - sourceplusplus/SourceMarker
diff --git a/layouts/certificate/baseof.html b/layouts/certificate/baseof.html
new file mode 100644
index 000000000000..326d446bbdfc
--- /dev/null
+++ b/layouts/certificate/baseof.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="{{ partial "seo/lang.html" . }}" class="no-js">
+ <head>
+ {{ partial "head.html" . }}
+ {{- /* Certificates print on one A4 landscape page; scoped here so other pages keep their print layout. */}}
+ <style media="print">@page { size: A4 landscape; margin: 0; }</style>
+ </head>
+ <body class="td-{{ .Kind }} td-certificate">
+ <header>
+ {{ partial "navbar.html" . }}
+ </header>
+ <div class="container-fluid td-outer">
+ <main class="td-main" role="main">
+ {{ block "main" . }}{{ end }}
+ </main>
+ {{ partial "footer.html" . }}
+ </div>
+ {{ partial "event-popup.html" . }}
+ {{ partial "lightbox.html" . }}
+ {{ partial "sidebar-skywalking.html" . }}
+ {{ partial "scripts.html" . }}
+ </body>
+</html>
diff --git a/layouts/contributors/list.html b/layouts/contributors/list.html
new file mode 100644
index 000000000000..cf0cef7cf743
--- /dev/null
+++ b/layouts/contributors/list.html
@@ -0,0 +1,4 @@
+{{ define "main" }}
+{{- /* The contributors shortcodes carry their own full-width hero and container. */ -}}
+{{ .Content }}
+{{ end }}
diff --git a/layouts/shortcodes/certificate.html b/layouts/shortcodes/certificate.html
new file mode 100644
index 000000000000..05266782b0b4
--- /dev/null
+++ b/layouts/shortcodes/certificate.html
@@ -0,0 +1,60 @@
+{{- /* /certificate — Certificate of Contribution, rendered in the visitor's browser from
+ assets/certificate/people.json, which scripts/certificate.js builds from data/team.yml,
+ data/committee.yml and data/repositories.yml. The same script writes the day's issued
+ certificate codes to certificate/verify/<date>.txt, the only certificate data the site
+ keeps. Drawing, model and verification live in assets/js/certificate/. */ -}}
+{{- $people := resources.Get "certificate/people.json" -}}
+{{- if not $people -}}
+ {{- warnf "certificate: assets/certificate/people.json is missing; run npm run certificate" -}}
+{{- end -}}
+<section class="cert-hero">
+ <div class="container">
+ <span class="eyebrow">CERTIFICATE</span>
+ <h1>Certificate of Contribution</h1>
+ <p>Every PMC member, committer and contributor listed on this site can get an Apache SkyWalking
+ certificate. It is drawn in your browser from the project's public contribution data, and its
+ QR code links back here, where anyone can verify it.</p>
+ <form class="cert-search" role="search" autocomplete="off">
+ <input type="search" name="q" placeholder="GitHub login or Apache ID" aria-label="GitHub login or Apache ID" spellcheck="false">
+ <button type="submit">Get certificate</button>
+ </form>
+ </div>
+</section>
+
+<div class="container cert-wrap" id="cert-app"
+ data-people="{{ with $people }}{{ (. | fingerprint).RelPermalink }}{{ end }}"
+ data-verify="{{ "certificate/verify/" | relURL }}"
+ data-base="{{ site.BaseURL }}"
+ data-logo="{{ "images/logo.svg" | relURL }}"
+ data-logo-white="{{ "favicons/logo.svg" | relURL }}"
+ data-asf-logo="{{ "images/certificate/asf-logo-horizontal-color.svg" | relURL }}"
+ data-asf-leaf="{{ "images/asf-oakleaf.svg" | relURL }}"
+ data-icons="{{ "images/project/" | relURL }}">
+ <div class="cert-status idle" role="status" aria-live="polite"></div>
+ <div class="cert-verify" hidden>
+ <div class="cert-stage"><div class="cert-canvas cert-verify-canvas"></div></div>
+ <p class="cert-verify-today"></p>
+ </div>
+ <div class="cert-result" hidden>
+ <div class="cert-toolbar">
+ <div class="cert-actions">
+ <button type="button" class="cert-png">Download PNG</button>
+ <button type="button" class="cert-print">Print / Save PDF</button>
+ <button type="button" class="cert-copy"><span class="label">Copy verify link</span><span class="done">Copied</span></button>
+ </div>
+ </div>
+ <div class="cert-stage"><div class="cert-canvas"></div></div>
+ </div>
+ <p class="cert-note">Contributions count the commits GitHub attributes to each account on every
+ repository listed on this site, refreshed daily. Members are listed on the
+ <a href="{{ "team/" | relURL }}">team page</a>; every contributor is searchable on the
+ <a href="{{ "contributors/" | relURL }}">contributors page</a>.</p>
+</div>
+
+{{- /* Designs register themselves on SWCert.designs, so the runtime goes first and the controller last. */ -}}
+{{- $parts := slice (resources.Get "js/certificate/runtime.js") (resources.Get "js/certificate/model.js") -}}
+{{- $parts = $parts | append (resources.Match "js/certificate/designs/*.js") -}}
+{{- $parts = $parts | append (resources.Get "js/certificate/page.js") -}}
+{{- $certificateScript := $parts | resources.Concat "js/certificate.js" | minify | fingerprint }}
+<script src="{{ "js/qrcode-generator-1.4.4.js" | relURL }}"></script>
+<script src="{{ $certificateScript.RelPermalink }}" integrity="{{ $certificateScript.Data.Integrity }}"></script>
diff --git a/layouts/shortcodes/contributors-search.html b/layouts/shortcodes/contributors-search.html
index 07c3c9ab70b1..79624f788b69 100644
--- a/layouts/shortcodes/contributors-search.html
+++ b/layouts/shortcodes/contributors-search.html
@@ -8,6 +8,7 @@
<h1 class="page-title">Contributions by <span class="current-contributor"></span></h1>
<p>Per-project contribution counts and rankings. Search a different handle on the
<a href="/team/">team page</a>.</p>
+ <a class="cert-cta" href="{{ "certificate/" | relURL }}" hidden>Get your Certificate of Contribution →</a>
</div>
</section>
diff --git a/layouts/team/list.html b/layouts/team/list.html
index eb7cdfbe5943..ffe833332962 100644
--- a/layouts/team/list.html
+++ b/layouts/team/list.html
@@ -1,12 +1,32 @@
{{ define "main" }}
<section class="team-hero">
- <div class="container">
- <span class="eyebrow">TEAM</span>
- <h1>The people behind SkyWalking</h1>
- <p>The project is run by its Members (PMC & Committers) and grown by
- <a href="/contributors/">{{ .Site.Data.team.totalCount | default "1,000+" }} contributors</a>.
- Members have direct access to the source and actively evolve the codebase; contributors
- improve the project through patches and suggestions.</p>
+ <div class="container team-hero-row">
+ <div class="team-hero-text">
+ <span class="eyebrow">TEAM</span>
+ <h1>The people behind SkyWalking</h1>
+ <p>The project is run by its Members (PMC & Committers) and grown by
+ <a href="/contributors/">{{ .Site.Data.team.totalCount | default "1,000+" }} contributors</a>.
+ Members have direct access to the source and actively evolve the codebase; contributors
+ improve the project through patches and suggestions.</p>
+ </div>
+ <a class="cert-badge" href="{{ "certificate/" | relURL }}">
+ <svg class="cert-badge-seal" viewBox="0 0 64 72" width="56" height="63" aria-hidden="true">
+ <defs><linearGradient id="cert-badge-grad" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#479EEB"/><stop offset="1" stop-color="#2c6cb0"/></linearGradient></defs>
+ <polygon points="22,44 15,69 23,64 28,70 31,49" fill="#245f9d"/>
+ <polygon points="42,44 49,69 41,64 36,70 33,49" fill="#245f9d"/>
+ <polygon points="32.0,3.0 35.2,5.6 39.0,3.9 41.4,7.3 45.5,6.6 47.0,10.5 51.1,10.9 51.5,15.0 55.4,16.5 54.7,20.6 58.1,23.0 56.4,26.8 59.0,30.0 56.4,33.2 58.1,37.0 54.7,39.4 55.4,43.5 51.5,45.0 51.1,49.1 47.0,49.5 45.5,53.4 41.4,52.7 39.0,56.1 35.2,54.4 32.0,57.0 28.8,54.4 25.0,56.1 22.6,52.7 18.5,53.4 17.0,49.5 12.9,49.1 12.5,45.0 8.6,43.5 9.3,39.4 5.9,37.0 7.6,33.2 5.0,30.0 7.6,26.8 5.9,23.0 9.3,20.6 8.6,16.5 12.5,15.0 12.9,10.9 17.0,10.5 18.5,6.6 22.6,7.3 25.0,3.9 28.8,5.6" fill [...]
+ <circle cx="32" cy="30" r="20" fill="none" stroke="#fff" stroke-opacity=".55"/>
+ <polygon points="32.0,15.0 42.6,19.4 47.0,30.0 42.6,40.6 32.0,45.0 21.4,40.6 17.0,30.0 21.4,19.4" fill="none" stroke="#fff" stroke-opacity=".35"/>
+ <polygon points="32.0,17.0 38.7,23.3 44.0,30.0 37.3,35.3 32.0,41.0 26.0,36.0 19.5,30.0 24.9,22.9" fill="#fff" fill-opacity=".92"/>
+ <circle cx="32" cy="30" r="2.2" fill="#2c6cb0"/>
+ </svg>
+ <span class="cert-badge-text">
+ <span class="cert-badge-kicker">Certificate of Contribution</span>
+ <strong>Get your certificate</strong>
+ <span class="cert-badge-sub">For every PMC member, committer and contributor</span>
+ </span>
+ <span class="cert-badge-arrow" aria-hidden="true">→</span>
+ </a>
</div>
</section>
diff --git a/package.json b/package.json
index dd72491726ff..c156ace05255 100644
--- a/package.json
+++ b/package.json
@@ -4,15 +4,16 @@
"description": "SkyWalking Website",
"main": "none.js",
"scripts": {
- "build": "npm install && npm run stars && hugo",
- "build-with-docs": "npm install && npm run docs && npm run team && npm run stars && hugo && npm run cp",
- "build-preview": "npm install && npm run docs && npm run team && npm run stars && hugo && npm run cp",
+ "build": "npm install && npm run stars && npm run certificate && hugo",
+ "build-with-docs": "npm install && npm run docs && npm run team && npm run certificate && npm run stars && hugo && npm run cp",
+ "build-preview": "npm install && npm run docs && npm run team && npm run certificate && npm run stars && hugo && npm run cp",
"docs": "node ./docs.js",
"team": "node ./scripts/team.js",
+ "certificate": "node ./scripts/certificate.js",
"stars": "node ./scripts/stars.js",
"cp": "cp ./.htaccess ./public && cp ./.asf.yaml ./public",
"validate-projects": "node ./scripts/project-config.js",
- "test": "node --test ./scripts/project-config.test.js"
+ "test": "node --test ./scripts/project-config.test.js ./scripts/certificate.test.js"
},
"repository": {
"type": "git",
@@ -25,6 +26,7 @@
"homepage": "skywalking.apache.org",
"dependencies": {
"axios": "^0.21.1",
+ "js-yaml": "^3.14.1",
"yamljs": "^0.3.0"
},
"devDependencies": {
diff --git a/scripts/certificate.js b/scripts/certificate.js
new file mode 100644
index 000000000000..e7ec348862bb
--- /dev/null
+++ b/scripts/certificate.js
@@ -0,0 +1,104 @@
+// Builds the data of the Certificate of Contribution (/certificate/):
+//
+// - assets/certificate/people.json: today's contributors per repository, grouped into the
+// contribution categories of data/repositories.yml, plus the PMC and committers. The page
+// draws certificates from it and it is rebuilt every time.
+// - static/certificate/verify/<date>.txt: the code of every certificate that can be issued
+// today, after a header line naming the day's categories in polygon order, so a verified
+// polygon is labelled as it was issued. This is the only certificate data the site keeps:
+// the deploy carries certificate/verify/ forward on asf-site, so a certificate stays
+// verifiable for good.
+//
+// Run after `npm run team`; without data/team.yml only members are listed.
+const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
+// js-yaml, not yamljs: yamljs reads an unquoted login such as 0xAlanYin as a hex number.
+const yaml = require('js-yaml');
+const {loadProjectConfig} = require('./project-config');
+const model = require('../assets/js/certificate/model');
+
+const root = path.join(__dirname, '..');
+
+function buildPeople(team, committee, catalog, date) {
+ const categoryOf = {};
+ catalog.categories.forEach(category => category.repos.forEach(repo => { categoryOf[repo.toLowerCase()] = category.id; }));
+ const buckets = {};
+ for (const group of team.projects || []) {
+ for (const item of group.list || []) {
+ if (!item.repo) continue;
+ const category = categoryOf[`${item.user}/${item.repo}`.toLowerCase()];
+ if (!category) throw new Error(`certificate: ${item.user}/${item.repo} is not listed in data/repositories.yml`);
+ const contributors = item.contributors || [];
+ // Only GitHub accounts with a commit count get a certificate: anonymous commits, bots and
+ // name-only acknowledgements (projects.yml extraContributors, e.g. "Adrian Cole") cannot be
+ // verified. Ranks keep their position in the full list.
+ const rows = contributors
+ .map((c, i) => ({l: String(c.login), n: c.contributions, r: i + 1, type: c.type}))
+ .filter(row => row.type !== 'Anonymous' && !row.l.endsWith('[bot]') && Number.isInteger(row.n) && row.n > 0)
+ .map(({l, n, r}) => ({l, n, r}));
+ (buckets[category] = buckets[category] || []).push({
+ name: item.name,
+ label: item.name.replace(/^\((Archived|Retired|Experimental|outdated)\)\s*/, ''),
+ archived: /^\((Archived|Retired|outdated)\)/.test(item.name),
+ icon: item.icon || null,
+ of: contributors.length,
+ contributors: rows,
+ });
+ }
+ }
+ return {
+ generated: date,
+ totalPeople: team.totalCount || 0,
+ catalogs: catalog.categories.map(c => ({id: c.id, name: c.name, repos: buckets[c.id] || []})),
+ members: {pmc: committee.pmc || [], committer: committee.committer || []},
+ };
+}
+
+function codeFor(date, facts) {
+ const digest = crypto.createHash('sha256').update(model.canonical(date, facts), 'utf8').digest();
+ return model.encode(digest);
+}
+
+function issuedCodes(people, date) {
+ const idx = model.index(people);
+ return model.everyone(idx)
+ .map(query => codeFor(date, model.facts(people, idx, model.resolve(idx, query))))
+ .sort();
+}
+
+// The day file: a '#' header with the categories in order, then one code per line.
+function dayFile(people, date) {
+ const header = '# ' + JSON.stringify({categories: people.catalogs.map(c => c.name)});
+ return [header, ...issuedCodes(people, date)].join('\n') + '\n';
+}
+
+function main() {
+ const date = new Date().toISOString().slice(0, 10);
+ const config = loadProjectConfig(root);
+ const teamFile = path.join(root, 'data/team.yml');
+ const load = file => yaml.safeLoad(fs.readFileSync(file, 'utf8'));
+ const team = fs.existsSync(teamFile) ? load(teamFile) : {};
+ const committee = load(path.join(root, 'data/committee.yml'));
+ const people = buildPeople(team, committee, config.repositoryCatalog, date);
+
+ fs.mkdirSync(path.join(root, 'assets/certificate'), {recursive: true});
+ fs.writeFileSync(path.join(root, 'assets/certificate/people.json'), JSON.stringify(people));
+ const verifyDir = path.join(root, 'static/certificate/verify');
+ fs.mkdirSync(verifyDir, {recursive: true});
+ const day = dayFile(people, date);
+ fs.writeFileSync(path.join(verifyDir, `${date}.txt`), day);
+ const count = day.split('\n').filter(line => line && !line.startsWith('#')).length;
+ console.log(`certificate: ${count} certificate codes for ${date}${team.projects ? '' : ' (no data/team.yml: members only)'}.`);
+}
+
+module.exports = {buildPeople, codeFor, issuedCodes, dayFile};
+
+if (require.main === module) {
+ try {
+ main();
+ } catch (error) {
+ console.error(error.message);
+ process.exitCode = 1;
+ }
+}
diff --git a/scripts/certificate.test.js b/scripts/certificate.test.js
new file mode 100644
index 000000000000..f632407fb305
--- /dev/null
+++ b/scripts/certificate.test.js
@@ -0,0 +1,96 @@
+const assert = require('node:assert/strict');
+const test = require('node:test');
+const {buildPeople, codeFor, issuedCodes, dayFile} = require('./certificate');
+const model = require('../assets/js/certificate/model');
+
+const catalog = {categories: [
+ {id: 'main', name: 'Main', repos: ['apache/main']},
+ {id: 'agent', name: 'Agent', repos: ['apache/agent', 'SkyAPM/Old-Agent']},
+]};
+const committee = {pmc: [{name: 'Pat Lead (Project V.P.)', apacheId: 'pat', github: 'pat-gh'}], committer: [{name: 'Cy Commit', apacheId: 'cy'}]};
+
+function team() {
+ const person = (login, contributions, type = 'User') => ({login, contributions, type});
+ return {totalCount: 6, projects: [{type: 'Platform', list: [
+ {name: null, descriptionItem: 'Catalog note'},
+ {name: 'Main', user: 'apache', repo: 'main', icon: 'main', contributors: [
+ person('pat-gh', 50), person('someone**', 9, 'Anonymous'), person('ann', 5), person('dependabot[bot]', 4), person('0xAlanYin', 1),
+ // Appended by scripts/team.js from projects.yml extraContributors: a name, no count.
+ {login: 'Adrian Cole'}, {login: 'github-actions[bot]'},
+ ]},
+ ]}, {type: 'Agents', list: [
+ {name: 'Agent', user: 'apache', repo: 'agent', contributors: [person('ann', 7), person('pat-gh', 2)]},
+ {name: '(Archived) Old Agent', user: 'SkyAPM', repo: 'old-agent', contributors: [person('ann', 1)]},
+ ]}]};
+}
+
+test('people data keeps verifiable contributors, their ranks and the catalog order', () => {
+ const people = buildPeople(team(), committee, catalog, '2026-10-20');
+ assert.equal(people.generated, '2026-10-20');
+ assert.deepEqual(people.catalogs.map(c => c.id), ['main', 'agent']);
+ const main = people.catalogs[0].repos[0];
+ // Anonymous commits, bots and name-only extras are dropped; ranks keep their position.
+ assert.deepEqual(main.contributors, [{l: 'pat-gh', n: 50, r: 1}, {l: 'ann', n: 5, r: 3}, {l: '0xAlanYin', n: 1, r: 5}]);
+ assert.equal(main.of, 7);
+ const old = people.catalogs[1].repos[1];
+ assert.equal(old.label, 'Old Agent');
+ assert.equal(old.archived, true);
+ assert.equal(old.icon, null);
+});
+
+test('a repository missing from data/repositories.yml stops the build', () => {
+ const data = team();
+ data.projects[1].list.push({name: 'New', user: 'apache', repo: 'new', contributors: []});
+ assert.throws(() => buildPeople(data, committee, catalog, '2026-10-20'), /apache\/new is not listed in data\/repositories.yml/);
+});
+
+test('facts carry role, totals and the 10% rank band of every category', () => {
+ const people = buildPeople(team(), committee, catalog, '2026-10-20');
+ const idx = model.index(people);
+ const pat = model.facts(people, idx, model.resolve(idx, {contributor: 'pat-gh'}));
+ // A member's GitHub login resolves to the member.
+ assert.deepEqual(pat, {key: 'member:pat', role: 'p', total: 52, projects: 2, bands: 'a5'});
+ const ann = model.facts(people, idx, model.resolve(idx, {contributor: 'ANN'}));
+ // Main: ann is #2 of 3, band 10 - floor(1 * 10 / 3) = 7; Agent: #1 of 2, band 10.
+ assert.deepEqual(ann, {key: 'contributor:ann', role: 'x', total: 13, projects: 3, bands: '7a'});
+ // A member without GitHub data has no totals and empty bands.
+ const cy = model.facts(people, idx, model.resolve(idx, {member: 'cy'}));
+ assert.deepEqual(cy, {key: 'member:cy', role: 'c', total: null, projects: 0, bands: '00'});
+ assert.equal(model.canonical('2026-10-20', cy), 'v1|2026-10-20|member:cy|c||0|00');
+});
+
+test('codes are 80 bits of SHA-256 in Crockford base32, and change with any fact', () => {
+ assert.equal(model.encode(Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), '000G40R40M30E209');
+ const facts = {key: 'member:wusheng', role: 'p', total: 8193, projects: 48, bands: 'aaaaaaaa'};
+ assert.equal(codeFor('2026-10-20', facts), 'Z1JQ11JK2CGHW9DZ');
+ assert.notEqual(codeFor('2026-10-20', {...facts, total: 8194}), 'Z1JQ11JK2CGHW9DZ');
+ assert.notEqual(codeFor('2026-10-21', facts), 'Z1JQ11JK2CGHW9DZ');
+});
+
+test('the day list holds one code for every member and every other contributor', () => {
+ const people = buildPeople(team(), committee, catalog, '2026-10-20');
+ const codes = issuedCodes(people, '2026-10-20');
+ // pat (member, also pat-gh), cy (member), ann and 0xAlanYin (contributors).
+ assert.equal(codes.length, 4);
+ assert.deepEqual(codes, [...codes].sort());
+ const idx = model.index(people);
+ assert.ok(codes.includes(codeFor('2026-10-20', model.facts(people, idx, model.resolve(idx, {contributor: '0xalanyin'})))));
+});
+
+test('the day file names the day\'s categories in order before the codes', () => {
+ const people = buildPeople(team(), committee, catalog, '2026-10-20');
+ const lines = dayFile(people, '2026-10-20').trim().split('\n');
+ assert.equal(lines[0], '# {"categories":["Main","Agent"]}');
+ assert.deepEqual(lines.slice(1), issuedCodes(people, '2026-10-20'));
+});
+
+test('logins and IDs that are names of JavaScript object members are ordinary keys', () => {
+ const data = team();
+ data.projects[0].list[1].contributors.push({login: 'constructor', contributions: 3, type: 'User'}, {login: '__proto__', contributions: 2, type: 'User'});
+ const people = buildPeople(data, committee, catalog, '2026-10-20');
+ const idx = model.index(people);
+ assert.equal(model.facts(people, idx, model.resolve(idx, {contributor: 'constructor'})).total, 3);
+ assert.equal(model.facts(people, idx, model.resolve(idx, {q: '__proto__'})).total, 2);
+ assert.equal(model.resolve(idx, {member: 'constructor'}), null);
+ assert.equal(model.resolve(idx, {q: 'toString'}), null);
+});
diff --git a/scripts/project-config.js b/scripts/project-config.js
index 19ad133f442e..46880a2322fb 100644
--- a/scripts/project-config.js
+++ b/scripts/project-config.js
@@ -129,6 +129,34 @@ function validateProjectConfig(config) {
return config;
}
+// data/repositories.yml groups every counted repository into the contribution categories of
+// the Certificate of Contribution. It lists each repository of the site exactly once, and
+// nothing else, so a repository added to the site cannot go uncategorised.
+function validateRepositoryCatalog(catalog, config) {
+ check(isObject(catalog) && Array.isArray(catalog.categories) && catalog.categories.length > 0, 'data/repositories.yml requires a categories array');
+ const ids = new Set();
+ const listed = new Set();
+ catalog.categories.forEach((category, index) => {
+ const where = `data/repositories.yml categories[${index}]`;
+ check(isObject(category) && /^[a-z][a-z0-9-]*$/.test(category.id || '') && isText(category.name), `${where} requires a lower-case id and a name`);
+ check(!ids.has(category.id), `data/repositories.yml has duplicate category id ${category.id}`);
+ ids.add(category.id);
+ check(Array.isArray(category.repos) && category.repos.length > 0, `${where}.repos must be a nonempty array`);
+ category.repos.forEach(entry => {
+ check(typeof entry === 'string' && /^[a-z0-9_-]+\/[a-z0-9_.-]+$/i.test(entry), `${where}.repos entry ${entry} must be owner/repo`);
+ check(!listed.has(entry.toLowerCase()), `data/repositories.yml lists ${entry} more than once`);
+ listed.add(entry.toLowerCase());
+ });
+ });
+ const expected = collectRepos(config).map(({user, repo}) => `${user}/${repo}`);
+ const missing = expected.filter(name => !listed.has(name.toLowerCase()));
+ check(!missing.length, `add ${missing.join(', ')} to a category in data/repositories.yml`);
+ const known = new Set(expected.map(name => name.toLowerCase()));
+ const stale = [...listed].filter(name => !known.has(name));
+ check(!stale.length, `data/repositories.yml lists ${stale.join(', ')}, which the site no longer includes`);
+ return catalog;
+}
+
function websiteProject(packageInfo) {
const repository = typeof packageInfo.repository === 'string' ? packageInfo.repository : packageInfo.repository.url;
const url = new URL(repository.replace(/^git\+/, ''));
@@ -147,7 +175,9 @@ function loadProjectConfig(root = path.join(__dirname, '..')) {
const projects = YAML.load(path.join(root, 'data/projects.yml'));
const {showcase} = YAML.load(path.join(root, 'data/get-started.yml'));
const packageInfo = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
- return validateProjectConfig({...projects, showcase, website: websiteProject(packageInfo)});
+ const config = validateProjectConfig({...projects, showcase, website: websiteProject(packageInfo)});
+ config.repositoryCatalog = validateRepositoryCatalog(YAML.load(path.join(root, 'data/repositories.yml')), config);
+ return config;
}
function docsForProject(project) {
@@ -196,13 +226,13 @@ function collectRepos(config) {
.map(({user, repo}) => ({user, repo}));
}
-module.exports = {loadProjectConfig, validateProjectConfig, docsForProject, deriveDocsList, collectRepos, websiteProject};
+module.exports = {loadProjectConfig, validateProjectConfig, validateRepositoryCatalog, docsForProject, deriveDocsList, collectRepos, websiteProject};
if (require.main === module) {
try {
const config = loadProjectConfig();
const docs = deriveDocsList(config).flatMap(group => group.list).flatMap(project => project.docs || []);
- console.log(`Validated ${config.catalogs.length} catalogs, ${collectRepos(config).length} repositories and ${docs.length} documentation entries.`);
+ console.log(`Validated ${config.catalogs.length} catalogs, ${collectRepos(config).length} repositories in ${config.repositoryCatalog.categories.length} contribution categories and ${docs.length} documentation entries.`);
} catch (error) {
console.error(error.message);
process.exitCode = 1;
diff --git a/scripts/project-config.test.js b/scripts/project-config.test.js
index 8e3623428edc..e4b3464e3f4d 100644
--- a/scripts/project-config.test.js
+++ b/scripts/project-config.test.js
@@ -1,6 +1,6 @@
const assert = require('node:assert/strict');
const test = require('node:test');
-const {validateProjectConfig, docsForProject, deriveDocsList, collectRepos, websiteProject} = require('./project-config');
+const {loadProjectConfig, validateProjectConfig, validateRepositoryCatalog, docsForProject, deriveDocsList, collectRepos, websiteProject} = require('./project-config');
function fixture() {
return {
@@ -162,3 +162,26 @@ test('includes Showcase and package-derived Website in the team/stars repository
description: 'This is the repository including all source codes of https://example.apache.org', user: 'apache', repo: 'example-website',
});
});
+
+test('the repository catalog lists every repository of the site exactly once', () => {
+ const config = validateProjectConfig(fixture());
+ const catalog = repos => ({categories: [{id: 'main', name: 'Main', repos}]});
+ validateRepositoryCatalog(catalog(['apache/example', 'apache/example-showcase']), config);
+ // Owner and repository names compare case-insensitively, as GitHub does.
+ validateRepositoryCatalog(catalog(['Apache/Example', 'apache/example-showcase']), config);
+ assert.throws(() => validateRepositoryCatalog(catalog(['apache/example']), config), /add apache\/example-showcase to a category/);
+ assert.throws(() => validateRepositoryCatalog(catalog(['apache/example', 'apache/example-showcase', 'apache/example']), config), /more than once/);
+ assert.throws(() => validateRepositoryCatalog(catalog(['apache/example', 'apache/example-showcase', 'apache/gone']), config), /apache\/gone, which the site no longer includes/);
+ assert.throws(() => validateRepositoryCatalog(catalog(['example', 'apache/example-showcase']), config), /must be owner\/repo/);
+ assert.throws(() => validateRepositoryCatalog({categories: [
+ {id: 'main', name: 'Main', repos: ['apache/example']},
+ {id: 'main', name: 'Again', repos: ['apache/example-showcase']},
+ ]}, config), /duplicate category id main/);
+ assert.throws(() => validateRepositoryCatalog({categories: [{id: 'Main', name: 'Main', repos: ['apache/example', 'apache/example-showcase']}]}, config), /lower-case id/);
+});
+
+test('the checked-in configuration catalogs every repository', () => {
+ const config = loadProjectConfig();
+ const listed = config.repositoryCatalog.categories.flatMap(category => category.repos).length;
+ assert.equal(listed, collectRepos(config).length);
+});
diff --git a/static/images/certificate/asf-logo-horizontal-color.svg b/static/images/certificate/asf-logo-horizontal-color.svg
new file mode 100644
index 000000000000..f1084dfb369f
--- /dev/null
+++ b/static/images/certificate/asf-logo-horizontal-color.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 525">
+ <defs>
+ <style>
+ .cls-1 {
+ fill: #7c297d;
+ }
+
+ .cls-2 {
+ fill: #f79a23;
+ }
+
+ .cls-3 {
+ fill: #dd552c;
+ }
+
+ .cls-4 {
+ fill: #d22128;
+ }
+ </style>
+ </defs>
+ <g>
+ <g>
+ <path class="cls-1" d="M458.8041825,181.7454822h49.0451573l87.4128103,220.0939304h-53.4548455l-14.7775045-38.0445002h-87.4060779l-15.0939249,38.0445002h-53.1384251l87.4128103-220.0939304ZM514.4537735,324.4914631l-31.1236462-80.4919562-30.8139581,80.4919562h61.9376043Z"/>
+ <path class="cls-1" d="M622.7166664,331.7220052h48.7354693c.3164204,21.0655178,16.0364536,33.3318564,40.2459781,33.3318564,20.7490974,0,33.9579648-8.4894912,33.9579648-23.2669957,0-15.4103452-14.1513961-24.5259448-45.2750423-31.4467989-51.5697879-10.6909691-76.0957328-32.0661749-76.0957328-68.2256176,0-39.9295577,31.7564869-64.7719229,82.3837461-64.7719229,52.189164,0,85.5210204,28.2960599,85.5210204,72.6285734h-48.1093609c0-19.8065687-13.8349757-31.7564869-36.7855511-31.7564869-21 [...]
+ <path class="cls-1" d="M852.9091193,181.7454822h137.7169167v45.2750423h-87.7224983v55.0234826h79.8591155v44.0228255h-79.8591155v75.7725801h-49.9944184v-220.0939304Z"/>
+ </g>
+ <g>
+ <path class="cls-3" d="M141.1077177,209.8881008c13.1869872-32.608129,27.9005344-65.1523006,43.6878912-93.9970469-23.5665852-17.5041056-46.1956909-55.2490163-55.4930639-71.9721694-3.3392448,3.7936783-5.5070609,8.1629723-6.4899838,11.7176522-8.7587851,31.6252061,22.4204029,69.8144518-2.6929393,55.86166-20.9241385-11.6267655-68.040478-37.0885068-86.0124818-11.7816095,20.1280383,25.862316,72.7733189,90.9304622,107.0005776,110.1715137Z"/>
+ <path class="cls-2" d="M184.7956089,115.8910539c15.2975784-27.9493439,31.5999598-52.4281623,48.5015202-70.0635487,0,0-16.8578001,24.4013964-40.9360438,73.180626,14.5822664,4.0158458,56.1124399,12.2562401,113.9601427-2.686207,1.4222086-10.5125619-5.645074-22.0753701-40.8636711-25.9464703-22.9926525-2.5246306,27.5975787-54.9023003-9.0516423-79.6739759-1.1832102-.8011494-2.3462234-1.508046-3.4856733-2.1408868-1.2286536-.4409688-2.5263137-.8449097-3.9114944-1.2050903-42.8025874-11.1689 [...]
+ <path class="cls-4" d="M106.6818547,303.8380213c9.6626029-28.9457315,21.2860022-61.4663399,34.425863-93.9499204-34.2272587-19.2410514-86.8725393-84.3091977-107.0005776-110.1715137-3.5933909,5.0559936-6.0490149,12.1081284-6.8669953,21.7522173-4.3894911,51.802054,49.0518897,90.152876,38.3979485,97.1578844-14.0924881,9.2670774-42.1411341-22.2605096-53.19565-2.2284073,16.0229889,20.5841549,48.6614135,57.7770131,94.2394114,87.4397396Z"/>
+ <path class="cls-3" d="M254.7564893,192.1180675c-27.0589909-9.6508213,28.2758628-35.5333343,46.2445004-62.9912168,2.2990969-3.5109196,4.6756159-8.0485224,5.3202382-12.8049265-57.8477027,14.942447-99.3778763,6.7020527-113.9601427,2.686207-12.4649429,25.2564046-26.8587035,57.0835812-41.8701571,95.4579665,15.6341958,6.6852219,79.4703224,31.3794754,169.6366631,31.50739,15.1595653-39.4852227-39.6905594-44.6994265-65.3711019-53.8554202Z"/>
+ <path class="cls-4" d="M117.1371916,309.0488589c15.9001236,4.9550083,68.5841152,19.7560761,124.5366618,17.5276688,7.5166669-20.3518889-20.585838-22.3379316-22.8748364-38.6605101-1.7722907-12.6298854,73.9565291,10.6135471,98.3225807-35.3111668,1.2320197-2.3226602,2.1930625-4.5140395,3.0059935-6.6313631-90.1663407-.1279146-154.0024672-24.8221682-169.6366631-31.50739-10.9703615,28.0435968-22.2554604,59.5173251-33.3537365,94.5827612Z"/>
+ <path class="cls-1" d="M117.1371916,309.0488589c-7.1447046,22.5701977-14.1934733,46.6821031-21.0352222,72.1640414-2.4270116,9.0348114-4.8254106,18.2379315-7.190148,27.6430221,53.1064464,17.531035,102.0085414.0403941,103.6663821-21.6377674.0134647-.1716749-.018514-.2995895-.0084154-.464532,1.2639984-22.9606739-33.150083-10.2331694-32.3354689-24.0378496.8179803-13.9056654,60.0811593-.0807882,78.4082944-29.8949926,1.4053777-2.2856322,2.3209771-4.3154353,3.0312398-6.244253-55.9525467,2 [...]
+ <path class="cls-1" d="M12.4424433,216.3982816c-.7742201,1.4036946-1.4727012,3.0261906-2.068514,4.9651069-10.2920775,33.4496725,62.4846076,78.3847312,52.5830064,88.180298-8.9254108,8.8261086-20.5572255-11.3440069-34.9004936-3.0127259-1.5720033.9155994-3.1675699,2.0701971-4.8052136,3.749918-16.2215932,16.6188018-.2541461,64.4958967,45.7698699,90.0518908-10.7397786,36.0618236-21.4324308,76.351562-31.8995493,118.4859638,3.7970444-1.3296388,8.3413795-2.6626438,9.4724141-6.244253,1.6982 [...]
+ </g>
+ </g>
+ <g>
+ <polygon class="cls-1" points="404.1671788 144.7351535 413.43089 144.7351535 413.43089 82.5349876 436.7786739 82.5349876 436.7786739 73.4732468 380.8193949 73.4732468 380.8193949 82.5349876 404.1671788 82.5349876 404.1671788 144.7351535"/>
+ <polygon class="cls-1" points="464.8525663 112.9584696 501.321697 112.9584696 501.321697 144.7351535 510.6796611 144.7351535 510.6796611 73.4732468 501.321697 73.4732468 501.321697 103.8899964 464.8525663 103.8899964 464.8525663 73.4732468 455.5955874 73.4732468 455.5955874 144.7351535 464.8525663 144.7351535 464.8525663 112.9584696"/>
+ <polygon class="cls-1" points="581.40298 135.9629037 546.2331925 135.9629037 546.2331925 112.8574844 577.8213707 112.8574844 577.8213707 104.0852345 546.2331925 104.0852345 546.2331925 82.1512438 580.3392689 82.1512438 580.3392689 73.4732468 536.9762136 73.4732468 536.9762136 144.7351535 581.40298 144.7351535 581.40298 135.9629037"/>
+ </g>
+</svg>
\ No newline at end of file
diff --git a/themes/docsy/assets/scss/_styles_project.scss b/themes/docsy/assets/scss/_styles_project.scss
index e8b9b5a62024..745235426189 100644
--- a/themes/docsy/assets/scss/_styles_project.scss
+++ b/themes/docsy/assets/scss/_styles_project.scss
@@ -14,6 +14,7 @@ assets/scss/_styles_project.scss
@import "sidebar-menu";
@import "custom_team";
@import "custom_contributors";
+@import "custom_certificate";
@import "event-popup";
//common
diff --git a/themes/docsy/layouts/partials/script-search.html b/themes/docsy/layouts/partials/script-search.html
index 9d774d0eac5c..26b6543e9a94 100644
--- a/themes/docsy/layouts/partials/script-search.html
+++ b/themes/docsy/layouts/partials/script-search.html
@@ -20,6 +20,7 @@
}
})
$('.search-tips').hide();
+ $('.cert-cta').attr('href', $('.cert-cta').attr('href') + '?contributor=' + encodeURIComponent(contributor)).prop('hidden', false);
} else {
$('.search-tips').text('No data')
}
diff --git a/themes/docsy/static/js/qrcode-generator-1.4.4.js b/themes/docsy/static/js/qrcode-generator-1.4.4.js
new file mode 100644
index 000000000000..76889b589ec8
--- /dev/null
+++ b/themes/docsy/static/js/qrcode-generator-1.4.4.js
@@ -0,0 +1,2297 @@
+//---------------------------------------------------------------------
+//
+// QR Code Generator for JavaScript
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+// http://www.opensource.org/licenses/mit-license.php
+//
+// The word 'QR Code' is registered trademark of
+// DENSO WAVE INCORPORATED
+// http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+
+var qrcode = function() {
+
+ //---------------------------------------------------------------------
+ // qrcode
+ //---------------------------------------------------------------------
+
+ /**
+ * qrcode
+ * @param typeNumber 1 to 40
+ * @param errorCorrectionLevel 'L','M','Q','H'
+ */
+ var qrcode = function(typeNumber, errorCorrectionLevel) {
+
+ var PAD0 = 0xEC;
+ var PAD1 = 0x11;
+
+ var _typeNumber = typeNumber;
+ var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
+ var _modules = null;
+ var _moduleCount = 0;
+ var _dataCache = null;
+ var _dataList = [];
+
+ var _this = {};
+
+ var makeImpl = function(test, maskPattern) {
+
+ _moduleCount = _typeNumber * 4 + 17;
+ _modules = function(moduleCount) {
+ var modules = new Array(moduleCount);
+ for (var row = 0; row < moduleCount; row += 1) {
+ modules[row] = new Array(moduleCount);
+ for (var col = 0; col < moduleCount; col += 1) {
+ modules[row][col] = null;
+ }
+ }
+ return modules;
+ }(_moduleCount);
+
+ setupPositionProbePattern(0, 0);
+ setupPositionProbePattern(_moduleCount - 7, 0);
+ setupPositionProbePattern(0, _moduleCount - 7);
+ setupPositionAdjustPattern();
+ setupTimingPattern();
+ setupTypeInfo(test, maskPattern);
+
+ if (_typeNumber >= 7) {
+ setupTypeNumber(test);
+ }
+
+ if (_dataCache == null) {
+ _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList);
+ }
+
+ mapData(_dataCache, maskPattern);
+ };
+
+ var setupPositionProbePattern = function(row, col) {
+
+ for (var r = -1; r <= 7; r += 1) {
+
+ if (row + r <= -1 || _moduleCount <= row + r) continue;
+
+ for (var c = -1; c <= 7; c += 1) {
+
+ if (col + c <= -1 || _moduleCount <= col + c) continue;
+
+ if ( (0 <= r && r <= 6 && (c == 0 || c == 6) )
+ || (0 <= c && c <= 6 && (r == 0 || r == 6) )
+ || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ };
+
+ var getBestMaskPattern = function() {
+
+ var minLostPoint = 0;
+ var pattern = 0;
+
+ for (var i = 0; i < 8; i += 1) {
+
+ makeImpl(true, i);
+
+ var lostPoint = QRUtil.getLostPoint(_this);
+
+ if (i == 0 || minLostPoint > lostPoint) {
+ minLostPoint = lostPoint;
+ pattern = i;
+ }
+ }
+
+ return pattern;
+ };
+
+ var setupTimingPattern = function() {
+
+ for (var r = 8; r < _moduleCount - 8; r += 1) {
+ if (_modules[r][6] != null) {
+ continue;
+ }
+ _modules[r][6] = (r % 2 == 0);
+ }
+
+ for (var c = 8; c < _moduleCount - 8; c += 1) {
+ if (_modules[6][c] != null) {
+ continue;
+ }
+ _modules[6][c] = (c % 2 == 0);
+ }
+ };
+
+ var setupPositionAdjustPattern = function() {
+
+ var pos = QRUtil.getPatternPosition(_typeNumber);
+
+ for (var i = 0; i < pos.length; i += 1) {
+
+ for (var j = 0; j < pos.length; j += 1) {
+
+ var row = pos[i];
+ var col = pos[j];
+
+ if (_modules[row][col] != null) {
+ continue;
+ }
+
+ for (var r = -2; r <= 2; r += 1) {
+
+ for (var c = -2; c <= 2; c += 1) {
+
+ if (r == -2 || r == 2 || c == -2 || c == 2
+ || (r == 0 && c == 0) ) {
+ _modules[row + r][col + c] = true;
+ } else {
+ _modules[row + r][col + c] = false;
+ }
+ }
+ }
+ }
+ }
+ };
+
+ var setupTypeNumber = function(test) {
+
+ var bits = QRUtil.getBCHTypeNumber(_typeNumber);
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+ _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod;
+ }
+
+ for (var i = 0; i < 18; i += 1) {
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+ _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
+ }
+ };
+
+ var setupTypeInfo = function(test, maskPattern) {
+
+ var data = (_errorCorrectionLevel << 3) | maskPattern;
+ var bits = QRUtil.getBCHTypeInfo(data);
+
+ // vertical
+ for (var i = 0; i < 15; i += 1) {
+
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+
+ if (i < 6) {
+ _modules[i][8] = mod;
+ } else if (i < 8) {
+ _modules[i + 1][8] = mod;
+ } else {
+ _modules[_moduleCount - 15 + i][8] = mod;
+ }
+ }
+
+ // horizontal
+ for (var i = 0; i < 15; i += 1) {
+
+ var mod = (!test && ( (bits >> i) & 1) == 1);
+
+ if (i < 8) {
+ _modules[8][_moduleCount - i - 1] = mod;
+ } else if (i < 9) {
+ _modules[8][15 - i - 1 + 1] = mod;
+ } else {
+ _modules[8][15 - i - 1] = mod;
+ }
+ }
+
+ // fixed module
+ _modules[_moduleCount - 8][8] = (!test);
+ };
+
+ var mapData = function(data, maskPattern) {
+
+ var inc = -1;
+ var row = _moduleCount - 1;
+ var bitIndex = 7;
+ var byteIndex = 0;
+ var maskFunc = QRUtil.getMaskFunction(maskPattern);
+
+ for (var col = _moduleCount - 1; col > 0; col -= 2) {
+
+ if (col == 6) col -= 1;
+
+ while (true) {
+
+ for (var c = 0; c < 2; c += 1) {
+
+ if (_modules[row][col - c] == null) {
+
+ var dark = false;
+
+ if (byteIndex < data.length) {
+ dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1);
+ }
+
+ var mask = maskFunc(row, col - c);
+
+ if (mask) {
+ dark = !dark;
+ }
+
+ _modules[row][col - c] = dark;
+ bitIndex -= 1;
+
+ if (bitIndex == -1) {
+ byteIndex += 1;
+ bitIndex = 7;
+ }
+ }
+ }
+
+ row += inc;
+
+ if (row < 0 || _moduleCount <= row) {
+ row -= inc;
+ inc = -inc;
+ break;
+ }
+ }
+ }
+ };
+
+ var createBytes = function(buffer, rsBlocks) {
+
+ var offset = 0;
+
+ var maxDcCount = 0;
+ var maxEcCount = 0;
+
+ var dcdata = new Array(rsBlocks.length);
+ var ecdata = new Array(rsBlocks.length);
+
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+
+ var dcCount = rsBlocks[r].dataCount;
+ var ecCount = rsBlocks[r].totalCount - dcCount;
+
+ maxDcCount = Math.max(maxDcCount, dcCount);
+ maxEcCount = Math.max(maxEcCount, ecCount);
+
+ dcdata[r] = new Array(dcCount);
+
+ for (var i = 0; i < dcdata[r].length; i += 1) {
+ dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];
+ }
+ offset += dcCount;
+
+ var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
+ var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);
+
+ var modPoly = rawPoly.mod(rsPoly);
+ ecdata[r] = new Array(rsPoly.getLength() - 1);
+ for (var i = 0; i < ecdata[r].length; i += 1) {
+ var modIndex = i + modPoly.getLength() - ecdata[r].length;
+ ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0;
+ }
+ }
+
+ var totalCodeCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalCodeCount += rsBlocks[i].totalCount;
+ }
+
+ var data = new Array(totalCodeCount);
+ var index = 0;
+
+ for (var i = 0; i < maxDcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < dcdata[r].length) {
+ data[index] = dcdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ for (var i = 0; i < maxEcCount; i += 1) {
+ for (var r = 0; r < rsBlocks.length; r += 1) {
+ if (i < ecdata[r].length) {
+ data[index] = ecdata[r][i];
+ index += 1;
+ }
+ }
+ }
+
+ return data;
+ };
+
+ var createData = function(typeNumber, errorCorrectionLevel, dataList) {
+
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel);
+
+ var buffer = qrBitBuffer();
+
+ for (var i = 0; i < dataList.length; i += 1) {
+ var data = dataList[i];
+ buffer.put(data.getMode(), 4);
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
+ data.write(buffer);
+ }
+
+ // calc num max data.
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i += 1) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+
+ if (buffer.getLengthInBits() > totalDataCount * 8) {
+ throw 'code length overflow. ('
+ + buffer.getLengthInBits()
+ + '>'
+ + totalDataCount * 8
+ + ')';
+ }
+
+ // end code
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
+ buffer.put(0, 4);
+ }
+
+ // padding
+ while (buffer.getLengthInBits() % 8 != 0) {
+ buffer.putBit(false);
+ }
+
+ // padding
+ while (true) {
+
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD0, 8);
+
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break;
+ }
+ buffer.put(PAD1, 8);
+ }
+
+ return createBytes(buffer, rsBlocks);
+ };
+
+ _this.addData = function(data, mode) {
+
+ mode = mode || 'Byte';
+
+ var newData = null;
+
+ switch(mode) {
+ case 'Numeric' :
+ newData = qrNumber(data);
+ break;
+ case 'Alphanumeric' :
+ newData = qrAlphaNum(data);
+ break;
+ case 'Byte' :
+ newData = qr8BitByte(data);
+ break;
+ case 'Kanji' :
+ newData = qrKanji(data);
+ break;
+ default :
+ throw 'mode:' + mode;
+ }
+
+ _dataList.push(newData);
+ _dataCache = null;
+ };
+
+ _this.isDark = function(row, col) {
+ if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
+ throw row + ',' + col;
+ }
+ return _modules[row][col];
+ };
+
+ _this.getModuleCount = function() {
+ return _moduleCount;
+ };
+
+ _this.make = function() {
+ if (_typeNumber < 1) {
+ var typeNumber = 1;
+
+ for (; typeNumber < 40; typeNumber++) {
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, _errorCorrectionLevel);
+ var buffer = qrBitBuffer();
+
+ for (var i = 0; i < _dataList.length; i++) {
+ var data = _dataList[i];
+ buffer.put(data.getMode(), 4);
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
+ data.write(buffer);
+ }
+
+ var totalDataCount = 0;
+ for (var i = 0; i < rsBlocks.length; i++) {
+ totalDataCount += rsBlocks[i].dataCount;
+ }
+
+ if (buffer.getLengthInBits() <= totalDataCount * 8) {
+ break;
+ }
+ }
+
+ _typeNumber = typeNumber;
+ }
+
+ makeImpl(false, getBestMaskPattern() );
+ };
+
+ _this.createTableTag = function(cellSize, margin) {
+
+ cellSize = cellSize || 2;
+ margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+ var qrHtml = '';
+
+ qrHtml += '<table style="';
+ qrHtml += ' border-width: 0px; border-style: none;';
+ qrHtml += ' border-collapse: collapse;';
+ qrHtml += ' padding: 0px; margin: ' + margin + 'px;';
+ qrHtml += '">';
+ qrHtml += '<tbody>';
+
+ for (var r = 0; r < _this.getModuleCount(); r += 1) {
+
+ qrHtml += '<tr>';
+
+ for (var c = 0; c < _this.getModuleCount(); c += 1) {
+ qrHtml += '<td style="';
+ qrHtml += ' border-width: 0px; border-style: none;';
+ qrHtml += ' border-collapse: collapse;';
+ qrHtml += ' padding: 0px; margin: 0px;';
+ qrHtml += ' width: ' + cellSize + 'px;';
+ qrHtml += ' height: ' + cellSize + 'px;';
+ qrHtml += ' background-color: ';
+ qrHtml += _this.isDark(r, c)? '#000000' : '#ffffff';
+ qrHtml += ';';
+ qrHtml += '"/>';
+ }
+
+ qrHtml += '</tr>';
+ }
+
+ qrHtml += '</tbody>';
+ qrHtml += '</table>';
+
+ return qrHtml;
+ };
+
+ _this.createSvgTag = function(cellSize, margin, alt, title) {
+
+ var opts = {};
+ if (typeof arguments[0] == 'object') {
+ // Called by options.
+ opts = arguments[0];
+ // overwrite cellSize and margin.
+ cellSize = opts.cellSize;
+ margin = opts.margin;
+ alt = opts.alt;
+ title = opts.title;
+ }
+
+ cellSize = cellSize || 2;
+ margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+ // Compose alt property surrogate
+ alt = (typeof alt === 'string') ? {text: alt} : alt || {};
+ alt.text = alt.text || null;
+ alt.id = (alt.text) ? alt.id || 'qrcode-description' : null;
+
+ // Compose title property surrogate
+ title = (typeof title === 'string') ? {text: title} : title || {};
+ title.text = title.text || null;
+ title.id = (title.text) ? title.id || 'qrcode-title' : null;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var c, mc, r, mr, qrSvg='', rect;
+
+ rect = 'l' + cellSize + ',0 0,' + cellSize +
+ ' -' + cellSize + ',0 0,-' + cellSize + 'z ';
+
+ qrSvg += '<svg version="1.1" xmlns="http://www.w3.org/2000/svg"';
+ qrSvg += !opts.scalable ? ' width="' + size + 'px" height="' + size + 'px"' : '';
+ qrSvg += ' viewBox="0 0 ' + size + ' ' + size + '" ';
+ qrSvg += ' preserveAspectRatio="xMinYMin meet"';
+ qrSvg += (title.text || alt.text) ? ' role="img" aria-labelledby="' +
+ escapeXml([title.id, alt.id].join(' ').trim() ) + '"' : '';
+ qrSvg += '>';
+ qrSvg += (title.text) ? '<title id="' + escapeXml(title.id) + '">' +
+ escapeXml(title.text) + '</title>' : '';
+ qrSvg += (alt.text) ? '<description id="' + escapeXml(alt.id) + '">' +
+ escapeXml(alt.text) + '</description>' : '';
+ qrSvg += '<rect width="100%" height="100%" fill="white" cx="0" cy="0"/>';
+ qrSvg += '<path d="';
+
+ for (r = 0; r < _this.getModuleCount(); r += 1) {
+ mr = r * cellSize + margin;
+ for (c = 0; c < _this.getModuleCount(); c += 1) {
+ if (_this.isDark(r, c) ) {
+ mc = c*cellSize+margin;
+ qrSvg += 'M' + mc + ',' + mr + rect;
+ }
+ }
+ }
+
+ qrSvg += '" stroke="transparent" fill="black"/>';
+ qrSvg += '</svg>';
+
+ return qrSvg;
+ };
+
+ _this.createDataURL = function(cellSize, margin) {
+
+ cellSize = cellSize || 2;
+ margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ return createDataURL(size, size, function(x, y) {
+ if (min <= x && x < max && min <= y && y < max) {
+ var c = Math.floor( (x - min) / cellSize);
+ var r = Math.floor( (y - min) / cellSize);
+ return _this.isDark(r, c)? 0 : 1;
+ } else {
+ return 1;
+ }
+ } );
+ };
+
+ _this.createImgTag = function(cellSize, margin, alt) {
+
+ cellSize = cellSize || 2;
+ margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+
+ var img = '';
+ img += '<img';
+ img += '\u0020src="';
+ img += _this.createDataURL(cellSize, margin);
+ img += '"';
+ img += '\u0020width="';
+ img += size;
+ img += '"';
+ img += '\u0020height="';
+ img += size;
+ img += '"';
+ if (alt) {
+ img += '\u0020alt="';
+ img += escapeXml(alt);
+ img += '"';
+ }
+ img += '/>';
+
+ return img;
+ };
+
+ var escapeXml = function(s) {
+ var escaped = '';
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charAt(i);
+ switch(c) {
+ case '<': escaped += '<'; break;
+ case '>': escaped += '>'; break;
+ case '&': escaped += '&'; break;
+ case '"': escaped += '"'; break;
+ default : escaped += c; break;
+ }
+ }
+ return escaped;
+ };
+
+ var _createHalfASCII = function(margin) {
+ var cellSize = 1;
+ margin = (typeof margin == 'undefined')? cellSize * 2 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ var y, x, r1, r2, p;
+
+ var blocks = {
+ '██': '█',
+ '█ ': '▀',
+ ' █': '▄',
+ ' ': ' '
+ };
+
+ var blocksLastLineNoMargin = {
+ '██': '▀',
+ '█ ': '▀',
+ ' █': ' ',
+ ' ': ' '
+ };
+
+ var ascii = '';
+ for (y = 0; y < size; y += 2) {
+ r1 = Math.floor((y - min) / cellSize);
+ r2 = Math.floor((y + 1 - min) / cellSize);
+ for (x = 0; x < size; x += 1) {
+ p = '█';
+
+ if (min <= x && x < max && min <= y && y < max && _this.isDark(r1, Math.floor((x - min) / cellSize))) {
+ p = ' ';
+ }
+
+ if (min <= x && x < max && min <= y+1 && y+1 < max && _this.isDark(r2, Math.floor((x - min) / cellSize))) {
+ p += ' ';
+ }
+ else {
+ p += '█';
+ }
+
+ // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square.
+ ascii += (margin < 1 && y+1 >= max) ? blocksLastLineNoMargin[p] : blocks[p];
+ }
+
+ ascii += '\n';
+ }
+
+ if (size % 2 && margin > 0) {
+ return ascii.substring(0, ascii.length - size - 1) + Array(size+1).join('▀');
+ }
+
+ return ascii.substring(0, ascii.length-1);
+ };
+
+ _this.createASCII = function(cellSize, margin) {
+ cellSize = cellSize || 1;
+
+ if (cellSize < 2) {
+ return _createHalfASCII(margin);
+ }
+
+ cellSize -= 1;
+ margin = (typeof margin == 'undefined')? cellSize * 2 : margin;
+
+ var size = _this.getModuleCount() * cellSize + margin * 2;
+ var min = margin;
+ var max = size - margin;
+
+ var y, x, r, p;
+
+ var white = Array(cellSize+1).join('██');
+ var black = Array(cellSize+1).join(' ');
+
+ var ascii = '';
+ var line = '';
+ for (y = 0; y < size; y += 1) {
+ r = Math.floor( (y - min) / cellSize);
+ line = '';
+ for (x = 0; x < size; x += 1) {
+ p = 1;
+
+ if (min <= x && x < max && min <= y && y < max && _this.isDark(r, Math.floor((x - min) / cellSize))) {
+ p = 0;
+ }
+
+ // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square.
+ line += p ? white : black;
+ }
+
+ for (r = 0; r < cellSize; r += 1) {
+ ascii += line + '\n';
+ }
+ }
+
+ return ascii.substring(0, ascii.length-1);
+ };
+
+ _this.renderTo2dContext = function(context, cellSize) {
+ cellSize = cellSize || 2;
+ var length = _this.getModuleCount();
+ for (var row = 0; row < length; row++) {
+ for (var col = 0; col < length; col++) {
+ context.fillStyle = _this.isDark(row, col) ? 'black' : 'white';
+ context.fillRect(row * cellSize, col * cellSize, cellSize, cellSize);
+ }
+ }
+ }
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrcode.stringToBytes
+ //---------------------------------------------------------------------
+
+ qrcode.stringToBytesFuncs = {
+ 'default' : function(s) {
+ var bytes = [];
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charCodeAt(i);
+ bytes.push(c & 0xff);
+ }
+ return bytes;
+ }
+ };
+
+ qrcode.stringToBytes = qrcode.stringToBytesFuncs['default'];
+
+ //---------------------------------------------------------------------
+ // qrcode.createStringToBytes
+ //---------------------------------------------------------------------
+
+ /**
+ * @param unicodeData base64 string of byte array.
+ * [16bit Unicode],[16bit Bytes], ...
+ * @param numChars
+ */
+ qrcode.createStringToBytes = function(unicodeData, numChars) {
+
+ // create conversion map.
+
+ var unicodeMap = function() {
+
+ var bin = base64DecodeInputStream(unicodeData);
+ var read = function() {
+ var b = bin.read();
+ if (b == -1) throw 'eof';
+ return b;
+ };
+
+ var count = 0;
+ var unicodeMap = {};
+ while (true) {
+ var b0 = bin.read();
+ if (b0 == -1) break;
+ var b1 = read();
+ var b2 = read();
+ var b3 = read();
+ var k = String.fromCharCode( (b0 << 8) | b1);
+ var v = (b2 << 8) | b3;
+ unicodeMap[k] = v;
+ count += 1;
+ }
+ if (count != numChars) {
+ throw count + ' != ' + numChars;
+ }
+
+ return unicodeMap;
+ }();
+
+ var unknownChar = '?'.charCodeAt(0);
+
+ return function(s) {
+ var bytes = [];
+ for (var i = 0; i < s.length; i += 1) {
+ var c = s.charCodeAt(i);
+ if (c < 128) {
+ bytes.push(c);
+ } else {
+ var b = unicodeMap[s.charAt(i)];
+ if (typeof b == 'number') {
+ if ( (b & 0xff) == b) {
+ // 1byte
+ bytes.push(b);
+ } else {
+ // 2bytes
+ bytes.push(b >>> 8);
+ bytes.push(b & 0xff);
+ }
+ } else {
+ bytes.push(unknownChar);
+ }
+ }
+ }
+ return bytes;
+ };
+ };
+
+ //---------------------------------------------------------------------
+ // QRMode
+ //---------------------------------------------------------------------
+
+ var QRMode = {
+ MODE_NUMBER : 1 << 0,
+ MODE_ALPHA_NUM : 1 << 1,
+ MODE_8BIT_BYTE : 1 << 2,
+ MODE_KANJI : 1 << 3
+ };
+
+ //---------------------------------------------------------------------
+ // QRErrorCorrectionLevel
+ //---------------------------------------------------------------------
+
+ var QRErrorCorrectionLevel = {
+ L : 1,
+ M : 0,
+ Q : 3,
+ H : 2
+ };
+
+ //---------------------------------------------------------------------
+ // QRMaskPattern
+ //---------------------------------------------------------------------
+
+ var QRMaskPattern = {
+ PATTERN000 : 0,
+ PATTERN001 : 1,
+ PATTERN010 : 2,
+ PATTERN011 : 3,
+ PATTERN100 : 4,
+ PATTERN101 : 5,
+ PATTERN110 : 6,
+ PATTERN111 : 7
+ };
+
+ //---------------------------------------------------------------------
+ // QRUtil
+ //---------------------------------------------------------------------
+
+ var QRUtil = function() {
+
+ var PATTERN_POSITION_TABLE = [
+ [],
+ [6, 18],
+ [6, 22],
+ [6, 26],
+ [6, 30],
+ [6, 34],
+ [6, 22, 38],
+ [6, 24, 42],
+ [6, 26, 46],
+ [6, 28, 50],
+ [6, 30, 54],
+ [6, 32, 58],
+ [6, 34, 62],
+ [6, 26, 46, 66],
+ [6, 26, 48, 70],
+ [6, 26, 50, 74],
+ [6, 30, 54, 78],
+ [6, 30, 56, 82],
+ [6, 30, 58, 86],
+ [6, 34, 62, 90],
+ [6, 28, 50, 72, 94],
+ [6, 26, 50, 74, 98],
+ [6, 30, 54, 78, 102],
+ [6, 28, 54, 80, 106],
+ [6, 32, 58, 84, 110],
+ [6, 30, 58, 86, 114],
+ [6, 34, 62, 90, 118],
+ [6, 26, 50, 74, 98, 122],
+ [6, 30, 54, 78, 102, 126],
+ [6, 26, 52, 78, 104, 130],
+ [6, 30, 56, 82, 108, 134],
+ [6, 34, 60, 86, 112, 138],
+ [6, 30, 58, 86, 114, 142],
+ [6, 34, 62, 90, 118, 146],
+ [6, 30, 54, 78, 102, 126, 150],
+ [6, 24, 50, 76, 102, 128, 154],
+ [6, 28, 54, 80, 106, 132, 158],
+ [6, 32, 58, 84, 110, 136, 162],
+ [6, 26, 54, 82, 110, 138, 166],
+ [6, 30, 58, 86, 114, 142, 170]
+ ];
+ var G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0);
+ var G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0);
+ var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1);
+
+ var _this = {};
+
+ var getBCHDigit = function(data) {
+ var digit = 0;
+ while (data != 0) {
+ digit += 1;
+ data >>>= 1;
+ }
+ return digit;
+ };
+
+ _this.getBCHTypeInfo = function(data) {
+ var d = data << 10;
+ while (getBCHDigit(d) - getBCHDigit(G15) >= 0) {
+ d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15) ) );
+ }
+ return ( (data << 10) | d) ^ G15_MASK;
+ };
+
+ _this.getBCHTypeNumber = function(data) {
+ var d = data << 12;
+ while (getBCHDigit(d) - getBCHDigit(G18) >= 0) {
+ d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18) ) );
+ }
+ return (data << 12) | d;
+ };
+
+ _this.getPatternPosition = function(typeNumber) {
+ return PATTERN_POSITION_TABLE[typeNumber - 1];
+ };
+
+ _this.getMaskFunction = function(maskPattern) {
+
+ switch (maskPattern) {
+
+ case QRMaskPattern.PATTERN000 :
+ return function(i, j) { return (i + j) % 2 == 0; };
+ case QRMaskPattern.PATTERN001 :
+ return function(i, j) { return i % 2 == 0; };
+ case QRMaskPattern.PATTERN010 :
+ return function(i, j) { return j % 3 == 0; };
+ case QRMaskPattern.PATTERN011 :
+ return function(i, j) { return (i + j) % 3 == 0; };
+ case QRMaskPattern.PATTERN100 :
+ return function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; };
+ case QRMaskPattern.PATTERN101 :
+ return function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0; };
+ case QRMaskPattern.PATTERN110 :
+ return function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; };
+ case QRMaskPattern.PATTERN111 :
+ return function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; };
+
+ default :
+ throw 'bad maskPattern:' + maskPattern;
+ }
+ };
+
+ _this.getErrorCorrectPolynomial = function(errorCorrectLength) {
+ var a = qrPolynomial([1], 0);
+ for (var i = 0; i < errorCorrectLength; i += 1) {
+ a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0) );
+ }
+ return a;
+ };
+
+ _this.getLengthInBits = function(mode, type) {
+
+ if (1 <= type && type < 10) {
+
+ // 1 - 9
+
+ switch(mode) {
+ case QRMode.MODE_NUMBER : return 10;
+ case QRMode.MODE_ALPHA_NUM : return 9;
+ case QRMode.MODE_8BIT_BYTE : return 8;
+ case QRMode.MODE_KANJI : return 8;
+ default :
+ throw 'mode:' + mode;
+ }
+
+ } else if (type < 27) {
+
+ // 10 - 26
+
+ switch(mode) {
+ case QRMode.MODE_NUMBER : return 12;
+ case QRMode.MODE_ALPHA_NUM : return 11;
+ case QRMode.MODE_8BIT_BYTE : return 16;
+ case QRMode.MODE_KANJI : return 10;
+ default :
+ throw 'mode:' + mode;
+ }
+
+ } else if (type < 41) {
+
+ // 27 - 40
+
+ switch(mode) {
+ case QRMode.MODE_NUMBER : return 14;
+ case QRMode.MODE_ALPHA_NUM : return 13;
+ case QRMode.MODE_8BIT_BYTE : return 16;
+ case QRMode.MODE_KANJI : return 12;
+ default :
+ throw 'mode:' + mode;
+ }
+
+ } else {
+ throw 'type:' + type;
+ }
+ };
+
+ _this.getLostPoint = function(qrcode) {
+
+ var moduleCount = qrcode.getModuleCount();
+
+ var lostPoint = 0;
+
+ // LEVEL1
+
+ for (var row = 0; row < moduleCount; row += 1) {
+ for (var col = 0; col < moduleCount; col += 1) {
+
+ var sameCount = 0;
+ var dark = qrcode.isDark(row, col);
+
+ for (var r = -1; r <= 1; r += 1) {
+
+ if (row + r < 0 || moduleCount <= row + r) {
+ continue;
+ }
+
+ for (var c = -1; c <= 1; c += 1) {
+
+ if (col + c < 0 || moduleCount <= col + c) {
+ continue;
+ }
+
+ if (r == 0 && c == 0) {
+ continue;
+ }
+
+ if (dark == qrcode.isDark(row + r, col + c) ) {
+ sameCount += 1;
+ }
+ }
+ }
+
+ if (sameCount > 5) {
+ lostPoint += (3 + sameCount - 5);
+ }
+ }
+ };
+
+ // LEVEL2
+
+ for (var row = 0; row < moduleCount - 1; row += 1) {
+ for (var col = 0; col < moduleCount - 1; col += 1) {
+ var count = 0;
+ if (qrcode.isDark(row, col) ) count += 1;
+ if (qrcode.isDark(row + 1, col) ) count += 1;
+ if (qrcode.isDark(row, col + 1) ) count += 1;
+ if (qrcode.isDark(row + 1, col + 1) ) count += 1;
+ if (count == 0 || count == 4) {
+ lostPoint += 3;
+ }
+ }
+ }
+
+ // LEVEL3
+
+ for (var row = 0; row < moduleCount; row += 1) {
+ for (var col = 0; col < moduleCount - 6; col += 1) {
+ if (qrcode.isDark(row, col)
+ && !qrcode.isDark(row, col + 1)
+ && qrcode.isDark(row, col + 2)
+ && qrcode.isDark(row, col + 3)
+ && qrcode.isDark(row, col + 4)
+ && !qrcode.isDark(row, col + 5)
+ && qrcode.isDark(row, col + 6) ) {
+ lostPoint += 40;
+ }
+ }
+ }
+
+ for (var col = 0; col < moduleCount; col += 1) {
+ for (var row = 0; row < moduleCount - 6; row += 1) {
+ if (qrcode.isDark(row, col)
+ && !qrcode.isDark(row + 1, col)
+ && qrcode.isDark(row + 2, col)
+ && qrcode.isDark(row + 3, col)
+ && qrcode.isDark(row + 4, col)
+ && !qrcode.isDark(row + 5, col)
+ && qrcode.isDark(row + 6, col) ) {
+ lostPoint += 40;
+ }
+ }
+ }
+
+ // LEVEL4
+
+ var darkCount = 0;
+
+ for (var col = 0; col < moduleCount; col += 1) {
+ for (var row = 0; row < moduleCount; row += 1) {
+ if (qrcode.isDark(row, col) ) {
+ darkCount += 1;
+ }
+ }
+ }
+
+ var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;
+ lostPoint += ratio * 10;
+
+ return lostPoint;
+ };
+
+ return _this;
+ }();
+
+ //---------------------------------------------------------------------
+ // QRMath
+ //---------------------------------------------------------------------
+
+ var QRMath = function() {
+
+ var EXP_TABLE = new Array(256);
+ var LOG_TABLE = new Array(256);
+
+ // initialize tables
+ for (var i = 0; i < 8; i += 1) {
+ EXP_TABLE[i] = 1 << i;
+ }
+ for (var i = 8; i < 256; i += 1) {
+ EXP_TABLE[i] = EXP_TABLE[i - 4]
+ ^ EXP_TABLE[i - 5]
+ ^ EXP_TABLE[i - 6]
+ ^ EXP_TABLE[i - 8];
+ }
+ for (var i = 0; i < 255; i += 1) {
+ LOG_TABLE[EXP_TABLE[i] ] = i;
+ }
+
+ var _this = {};
+
+ _this.glog = function(n) {
+
+ if (n < 1) {
+ throw 'glog(' + n + ')';
+ }
+
+ return LOG_TABLE[n];
+ };
+
+ _this.gexp = function(n) {
+
+ while (n < 0) {
+ n += 255;
+ }
+
+ while (n >= 256) {
+ n -= 255;
+ }
+
+ return EXP_TABLE[n];
+ };
+
+ return _this;
+ }();
+
+ //---------------------------------------------------------------------
+ // qrPolynomial
+ //---------------------------------------------------------------------
+
+ function qrPolynomial(num, shift) {
+
+ if (typeof num.length == 'undefined') {
+ throw num.length + '/' + shift;
+ }
+
+ var _num = function() {
+ var offset = 0;
+ while (offset < num.length && num[offset] == 0) {
+ offset += 1;
+ }
+ var _num = new Array(num.length - offset + shift);
+ for (var i = 0; i < num.length - offset; i += 1) {
+ _num[i] = num[i + offset];
+ }
+ return _num;
+ }();
+
+ var _this = {};
+
+ _this.getAt = function(index) {
+ return _num[index];
+ };
+
+ _this.getLength = function() {
+ return _num.length;
+ };
+
+ _this.multiply = function(e) {
+
+ var num = new Array(_this.getLength() + e.getLength() - 1);
+
+ for (var i = 0; i < _this.getLength(); i += 1) {
+ for (var j = 0; j < e.getLength(); j += 1) {
+ num[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i) ) + QRMath.glog(e.getAt(j) ) );
+ }
+ }
+
+ return qrPolynomial(num, 0);
+ };
+
+ _this.mod = function(e) {
+
+ if (_this.getLength() - e.getLength() < 0) {
+ return _this;
+ }
+
+ var ratio = QRMath.glog(_this.getAt(0) ) - QRMath.glog(e.getAt(0) );
+
+ var num = new Array(_this.getLength() );
+ for (var i = 0; i < _this.getLength(); i += 1) {
+ num[i] = _this.getAt(i);
+ }
+
+ for (var i = 0; i < e.getLength(); i += 1) {
+ num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i) ) + ratio);
+ }
+
+ // recursive call
+ return qrPolynomial(num, 0).mod(e);
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // QRRSBlock
+ //---------------------------------------------------------------------
+
+ var QRRSBlock = function() {
+
+ var RS_BLOCK_TABLE = [
+
+ // L
+ // M
+ // Q
+ // H
+
+ // 1
+ [1, 26, 19],
+ [1, 26, 16],
+ [1, 26, 13],
+ [1, 26, 9],
+
+ // 2
+ [1, 44, 34],
+ [1, 44, 28],
+ [1, 44, 22],
+ [1, 44, 16],
+
+ // 3
+ [1, 70, 55],
+ [1, 70, 44],
+ [2, 35, 17],
+ [2, 35, 13],
+
+ // 4
+ [1, 100, 80],
+ [2, 50, 32],
+ [2, 50, 24],
+ [4, 25, 9],
+
+ // 5
+ [1, 134, 108],
+ [2, 67, 43],
+ [2, 33, 15, 2, 34, 16],
+ [2, 33, 11, 2, 34, 12],
+
+ // 6
+ [2, 86, 68],
+ [4, 43, 27],
+ [4, 43, 19],
+ [4, 43, 15],
+
+ // 7
+ [2, 98, 78],
+ [4, 49, 31],
+ [2, 32, 14, 4, 33, 15],
+ [4, 39, 13, 1, 40, 14],
+
+ // 8
+ [2, 121, 97],
+ [2, 60, 38, 2, 61, 39],
+ [4, 40, 18, 2, 41, 19],
+ [4, 40, 14, 2, 41, 15],
+
+ // 9
+ [2, 146, 116],
+ [3, 58, 36, 2, 59, 37],
+ [4, 36, 16, 4, 37, 17],
+ [4, 36, 12, 4, 37, 13],
+
+ // 10
+ [2, 86, 68, 2, 87, 69],
+ [4, 69, 43, 1, 70, 44],
+ [6, 43, 19, 2, 44, 20],
+ [6, 43, 15, 2, 44, 16],
+
+ // 11
+ [4, 101, 81],
+ [1, 80, 50, 4, 81, 51],
+ [4, 50, 22, 4, 51, 23],
+ [3, 36, 12, 8, 37, 13],
+
+ // 12
+ [2, 116, 92, 2, 117, 93],
+ [6, 58, 36, 2, 59, 37],
+ [4, 46, 20, 6, 47, 21],
+ [7, 42, 14, 4, 43, 15],
+
+ // 13
+ [4, 133, 107],
+ [8, 59, 37, 1, 60, 38],
+ [8, 44, 20, 4, 45, 21],
+ [12, 33, 11, 4, 34, 12],
+
+ // 14
+ [3, 145, 115, 1, 146, 116],
+ [4, 64, 40, 5, 65, 41],
+ [11, 36, 16, 5, 37, 17],
+ [11, 36, 12, 5, 37, 13],
+
+ // 15
+ [5, 109, 87, 1, 110, 88],
+ [5, 65, 41, 5, 66, 42],
+ [5, 54, 24, 7, 55, 25],
+ [11, 36, 12, 7, 37, 13],
+
+ // 16
+ [5, 122, 98, 1, 123, 99],
+ [7, 73, 45, 3, 74, 46],
+ [15, 43, 19, 2, 44, 20],
+ [3, 45, 15, 13, 46, 16],
+
+ // 17
+ [1, 135, 107, 5, 136, 108],
+ [10, 74, 46, 1, 75, 47],
+ [1, 50, 22, 15, 51, 23],
+ [2, 42, 14, 17, 43, 15],
+
+ // 18
+ [5, 150, 120, 1, 151, 121],
+ [9, 69, 43, 4, 70, 44],
+ [17, 50, 22, 1, 51, 23],
+ [2, 42, 14, 19, 43, 15],
+
+ // 19
+ [3, 141, 113, 4, 142, 114],
+ [3, 70, 44, 11, 71, 45],
+ [17, 47, 21, 4, 48, 22],
+ [9, 39, 13, 16, 40, 14],
+
+ // 20
+ [3, 135, 107, 5, 136, 108],
+ [3, 67, 41, 13, 68, 42],
+ [15, 54, 24, 5, 55, 25],
+ [15, 43, 15, 10, 44, 16],
+
+ // 21
+ [4, 144, 116, 4, 145, 117],
+ [17, 68, 42],
+ [17, 50, 22, 6, 51, 23],
+ [19, 46, 16, 6, 47, 17],
+
+ // 22
+ [2, 139, 111, 7, 140, 112],
+ [17, 74, 46],
+ [7, 54, 24, 16, 55, 25],
+ [34, 37, 13],
+
+ // 23
+ [4, 151, 121, 5, 152, 122],
+ [4, 75, 47, 14, 76, 48],
+ [11, 54, 24, 14, 55, 25],
+ [16, 45, 15, 14, 46, 16],
+
+ // 24
+ [6, 147, 117, 4, 148, 118],
+ [6, 73, 45, 14, 74, 46],
+ [11, 54, 24, 16, 55, 25],
+ [30, 46, 16, 2, 47, 17],
+
+ // 25
+ [8, 132, 106, 4, 133, 107],
+ [8, 75, 47, 13, 76, 48],
+ [7, 54, 24, 22, 55, 25],
+ [22, 45, 15, 13, 46, 16],
+
+ // 26
+ [10, 142, 114, 2, 143, 115],
+ [19, 74, 46, 4, 75, 47],
+ [28, 50, 22, 6, 51, 23],
+ [33, 46, 16, 4, 47, 17],
+
+ // 27
+ [8, 152, 122, 4, 153, 123],
+ [22, 73, 45, 3, 74, 46],
+ [8, 53, 23, 26, 54, 24],
+ [12, 45, 15, 28, 46, 16],
+
+ // 28
+ [3, 147, 117, 10, 148, 118],
+ [3, 73, 45, 23, 74, 46],
+ [4, 54, 24, 31, 55, 25],
+ [11, 45, 15, 31, 46, 16],
+
+ // 29
+ [7, 146, 116, 7, 147, 117],
+ [21, 73, 45, 7, 74, 46],
+ [1, 53, 23, 37, 54, 24],
+ [19, 45, 15, 26, 46, 16],
+
+ // 30
+ [5, 145, 115, 10, 146, 116],
+ [19, 75, 47, 10, 76, 48],
+ [15, 54, 24, 25, 55, 25],
+ [23, 45, 15, 25, 46, 16],
+
+ // 31
+ [13, 145, 115, 3, 146, 116],
+ [2, 74, 46, 29, 75, 47],
+ [42, 54, 24, 1, 55, 25],
+ [23, 45, 15, 28, 46, 16],
+
+ // 32
+ [17, 145, 115],
+ [10, 74, 46, 23, 75, 47],
+ [10, 54, 24, 35, 55, 25],
+ [19, 45, 15, 35, 46, 16],
+
+ // 33
+ [17, 145, 115, 1, 146, 116],
+ [14, 74, 46, 21, 75, 47],
+ [29, 54, 24, 19, 55, 25],
+ [11, 45, 15, 46, 46, 16],
+
+ // 34
+ [13, 145, 115, 6, 146, 116],
+ [14, 74, 46, 23, 75, 47],
+ [44, 54, 24, 7, 55, 25],
+ [59, 46, 16, 1, 47, 17],
+
+ // 35
+ [12, 151, 121, 7, 152, 122],
+ [12, 75, 47, 26, 76, 48],
+ [39, 54, 24, 14, 55, 25],
+ [22, 45, 15, 41, 46, 16],
+
+ // 36
+ [6, 151, 121, 14, 152, 122],
+ [6, 75, 47, 34, 76, 48],
+ [46, 54, 24, 10, 55, 25],
+ [2, 45, 15, 64, 46, 16],
+
+ // 37
+ [17, 152, 122, 4, 153, 123],
+ [29, 74, 46, 14, 75, 47],
+ [49, 54, 24, 10, 55, 25],
+ [24, 45, 15, 46, 46, 16],
+
+ // 38
+ [4, 152, 122, 18, 153, 123],
+ [13, 74, 46, 32, 75, 47],
+ [48, 54, 24, 14, 55, 25],
+ [42, 45, 15, 32, 46, 16],
+
+ // 39
+ [20, 147, 117, 4, 148, 118],
+ [40, 75, 47, 7, 76, 48],
+ [43, 54, 24, 22, 55, 25],
+ [10, 45, 15, 67, 46, 16],
+
+ // 40
+ [19, 148, 118, 6, 149, 119],
+ [18, 75, 47, 31, 76, 48],
+ [34, 54, 24, 34, 55, 25],
+ [20, 45, 15, 61, 46, 16]
+ ];
+
+ var qrRSBlock = function(totalCount, dataCount) {
+ var _this = {};
+ _this.totalCount = totalCount;
+ _this.dataCount = dataCount;
+ return _this;
+ };
+
+ var _this = {};
+
+ var getRsBlockTable = function(typeNumber, errorCorrectionLevel) {
+
+ switch(errorCorrectionLevel) {
+ case QRErrorCorrectionLevel.L :
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
+ case QRErrorCorrectionLevel.M :
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
+ case QRErrorCorrectionLevel.Q :
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
+ case QRErrorCorrectionLevel.H :
+ return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
+ default :
+ return undefined;
+ }
+ };
+
+ _this.getRSBlocks = function(typeNumber, errorCorrectionLevel) {
+
+ var rsBlock = getRsBlockTable(typeNumber, errorCorrectionLevel);
+
+ if (typeof rsBlock == 'undefined') {
+ throw 'bad rs block @ typeNumber:' + typeNumber +
+ '/errorCorrectionLevel:' + errorCorrectionLevel;
+ }
+
+ var length = rsBlock.length / 3;
+
+ var list = [];
+
+ for (var i = 0; i < length; i += 1) {
+
+ var count = rsBlock[i * 3 + 0];
+ var totalCount = rsBlock[i * 3 + 1];
+ var dataCount = rsBlock[i * 3 + 2];
+
+ for (var j = 0; j < count; j += 1) {
+ list.push(qrRSBlock(totalCount, dataCount) );
+ }
+ }
+
+ return list;
+ };
+
+ return _this;
+ }();
+
+ //---------------------------------------------------------------------
+ // qrBitBuffer
+ //---------------------------------------------------------------------
+
+ var qrBitBuffer = function() {
+
+ var _buffer = [];
+ var _length = 0;
+
+ var _this = {};
+
+ _this.getBuffer = function() {
+ return _buffer;
+ };
+
+ _this.getAt = function(index) {
+ var bufIndex = Math.floor(index / 8);
+ return ( (_buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1;
+ };
+
+ _this.put = function(num, length) {
+ for (var i = 0; i < length; i += 1) {
+ _this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1);
+ }
+ };
+
+ _this.getLengthInBits = function() {
+ return _length;
+ };
+
+ _this.putBit = function(bit) {
+
+ var bufIndex = Math.floor(_length / 8);
+ if (_buffer.length <= bufIndex) {
+ _buffer.push(0);
+ }
+
+ if (bit) {
+ _buffer[bufIndex] |= (0x80 >>> (_length % 8) );
+ }
+
+ _length += 1;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrNumber
+ //---------------------------------------------------------------------
+
+ var qrNumber = function(data) {
+
+ var _mode = QRMode.MODE_NUMBER;
+ var _data = data;
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return _data.length;
+ };
+
+ _this.write = function(buffer) {
+
+ var data = _data;
+
+ var i = 0;
+
+ while (i + 2 < data.length) {
+ buffer.put(strToNum(data.substring(i, i + 3) ), 10);
+ i += 3;
+ }
+
+ if (i < data.length) {
+ if (data.length - i == 1) {
+ buffer.put(strToNum(data.substring(i, i + 1) ), 4);
+ } else if (data.length - i == 2) {
+ buffer.put(strToNum(data.substring(i, i + 2) ), 7);
+ }
+ }
+ };
+
+ var strToNum = function(s) {
+ var num = 0;
+ for (var i = 0; i < s.length; i += 1) {
+ num = num * 10 + chatToNum(s.charAt(i) );
+ }
+ return num;
+ };
+
+ var chatToNum = function(c) {
+ if ('0' <= c && c <= '9') {
+ return c.charCodeAt(0) - '0'.charCodeAt(0);
+ }
+ throw 'illegal char :' + c;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrAlphaNum
+ //---------------------------------------------------------------------
+
+ var qrAlphaNum = function(data) {
+
+ var _mode = QRMode.MODE_ALPHA_NUM;
+ var _data = data;
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return _data.length;
+ };
+
+ _this.write = function(buffer) {
+
+ var s = _data;
+
+ var i = 0;
+
+ while (i + 1 < s.length) {
+ buffer.put(
+ getCode(s.charAt(i) ) * 45 +
+ getCode(s.charAt(i + 1) ), 11);
+ i += 2;
+ }
+
+ if (i < s.length) {
+ buffer.put(getCode(s.charAt(i) ), 6);
+ }
+ };
+
+ var getCode = function(c) {
+
+ if ('0' <= c && c <= '9') {
+ return c.charCodeAt(0) - '0'.charCodeAt(0);
+ } else if ('A' <= c && c <= 'Z') {
+ return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10;
+ } else {
+ switch (c) {
+ case ' ' : return 36;
+ case '$' : return 37;
+ case '%' : return 38;
+ case '*' : return 39;
+ case '+' : return 40;
+ case '-' : return 41;
+ case '.' : return 42;
+ case '/' : return 43;
+ case ':' : return 44;
+ default :
+ throw 'illegal char :' + c;
+ }
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qr8BitByte
+ //---------------------------------------------------------------------
+
+ var qr8BitByte = function(data) {
+
+ var _mode = QRMode.MODE_8BIT_BYTE;
+ var _data = data;
+ var _bytes = qrcode.stringToBytes(data);
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return _bytes.length;
+ };
+
+ _this.write = function(buffer) {
+ for (var i = 0; i < _bytes.length; i += 1) {
+ buffer.put(_bytes[i], 8);
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // qrKanji
+ //---------------------------------------------------------------------
+
+ var qrKanji = function(data) {
+
+ var _mode = QRMode.MODE_KANJI;
+ var _data = data;
+
+ var stringToBytes = qrcode.stringToBytesFuncs['SJIS'];
+ if (!stringToBytes) {
+ throw 'sjis not supported.';
+ }
+ !function(c, code) {
+ // self test for sjis support.
+ var test = stringToBytes(c);
+ if (test.length != 2 || ( (test[0] << 8) | test[1]) != code) {
+ throw 'sjis not supported.';
+ }
+ }('\u53cb', 0x9746);
+
+ var _bytes = stringToBytes(data);
+
+ var _this = {};
+
+ _this.getMode = function() {
+ return _mode;
+ };
+
+ _this.getLength = function(buffer) {
+ return ~~(_bytes.length / 2);
+ };
+
+ _this.write = function(buffer) {
+
+ var data = _bytes;
+
+ var i = 0;
+
+ while (i + 1 < data.length) {
+
+ var c = ( (0xff & data[i]) << 8) | (0xff & data[i + 1]);
+
+ if (0x8140 <= c && c <= 0x9FFC) {
+ c -= 0x8140;
+ } else if (0xE040 <= c && c <= 0xEBBF) {
+ c -= 0xC140;
+ } else {
+ throw 'illegal char at ' + (i + 1) + '/' + c;
+ }
+
+ c = ( (c >>> 8) & 0xff) * 0xC0 + (c & 0xff);
+
+ buffer.put(c, 13);
+
+ i += 2;
+ }
+
+ if (i < data.length) {
+ throw 'illegal char at ' + (i + 1);
+ }
+ };
+
+ return _this;
+ };
+
+ //=====================================================================
+ // GIF Support etc.
+ //
+
+ //---------------------------------------------------------------------
+ // byteArrayOutputStream
+ //---------------------------------------------------------------------
+
+ var byteArrayOutputStream = function() {
+
+ var _bytes = [];
+
+ var _this = {};
+
+ _this.writeByte = function(b) {
+ _bytes.push(b & 0xff);
+ };
+
+ _this.writeShort = function(i) {
+ _this.writeByte(i);
+ _this.writeByte(i >>> 8);
+ };
+
+ _this.writeBytes = function(b, off, len) {
+ off = off || 0;
+ len = len || b.length;
+ for (var i = 0; i < len; i += 1) {
+ _this.writeByte(b[i + off]);
+ }
+ };
+
+ _this.writeString = function(s) {
+ for (var i = 0; i < s.length; i += 1) {
+ _this.writeByte(s.charCodeAt(i) );
+ }
+ };
+
+ _this.toByteArray = function() {
+ return _bytes;
+ };
+
+ _this.toString = function() {
+ var s = '';
+ s += '[';
+ for (var i = 0; i < _bytes.length; i += 1) {
+ if (i > 0) {
+ s += ',';
+ }
+ s += _bytes[i];
+ }
+ s += ']';
+ return s;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // base64EncodeOutputStream
+ //---------------------------------------------------------------------
+
+ var base64EncodeOutputStream = function() {
+
+ var _buffer = 0;
+ var _buflen = 0;
+ var _length = 0;
+ var _base64 = '';
+
+ var _this = {};
+
+ var writeEncoded = function(b) {
+ _base64 += String.fromCharCode(encode(b & 0x3f) );
+ };
+
+ var encode = function(n) {
+ if (n < 0) {
+ // error.
+ } else if (n < 26) {
+ return 0x41 + n;
+ } else if (n < 52) {
+ return 0x61 + (n - 26);
+ } else if (n < 62) {
+ return 0x30 + (n - 52);
+ } else if (n == 62) {
+ return 0x2b;
+ } else if (n == 63) {
+ return 0x2f;
+ }
+ throw 'n:' + n;
+ };
+
+ _this.writeByte = function(n) {
+
+ _buffer = (_buffer << 8) | (n & 0xff);
+ _buflen += 8;
+ _length += 1;
+
+ while (_buflen >= 6) {
+ writeEncoded(_buffer >>> (_buflen - 6) );
+ _buflen -= 6;
+ }
+ };
+
+ _this.flush = function() {
+
+ if (_buflen > 0) {
+ writeEncoded(_buffer << (6 - _buflen) );
+ _buffer = 0;
+ _buflen = 0;
+ }
+
+ if (_length % 3 != 0) {
+ // padding
+ var padlen = 3 - _length % 3;
+ for (var i = 0; i < padlen; i += 1) {
+ _base64 += '=';
+ }
+ }
+ };
+
+ _this.toString = function() {
+ return _base64;
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // base64DecodeInputStream
+ //---------------------------------------------------------------------
+
+ var base64DecodeInputStream = function(str) {
+
+ var _str = str;
+ var _pos = 0;
+ var _buffer = 0;
+ var _buflen = 0;
+
+ var _this = {};
+
+ _this.read = function() {
+
+ while (_buflen < 8) {
+
+ if (_pos >= _str.length) {
+ if (_buflen == 0) {
+ return -1;
+ }
+ throw 'unexpected end of file./' + _buflen;
+ }
+
+ var c = _str.charAt(_pos);
+ _pos += 1;
+
+ if (c == '=') {
+ _buflen = 0;
+ return -1;
+ } else if (c.match(/^\s$/) ) {
+ // ignore if whitespace.
+ continue;
+ }
+
+ _buffer = (_buffer << 6) | decode(c.charCodeAt(0) );
+ _buflen += 6;
+ }
+
+ var n = (_buffer >>> (_buflen - 8) ) & 0xff;
+ _buflen -= 8;
+ return n;
+ };
+
+ var decode = function(c) {
+ if (0x41 <= c && c <= 0x5a) {
+ return c - 0x41;
+ } else if (0x61 <= c && c <= 0x7a) {
+ return c - 0x61 + 26;
+ } else if (0x30 <= c && c <= 0x39) {
+ return c - 0x30 + 52;
+ } else if (c == 0x2b) {
+ return 62;
+ } else if (c == 0x2f) {
+ return 63;
+ } else {
+ throw 'c:' + c;
+ }
+ };
+
+ return _this;
+ };
+
+ //---------------------------------------------------------------------
+ // gifImage (B/W)
+ //---------------------------------------------------------------------
+
+ var gifImage = function(width, height) {
+
+ var _width = width;
+ var _height = height;
+ var _data = new Array(width * height);
+
+ var _this = {};
+
+ _this.setPixel = function(x, y, pixel) {
+ _data[y * _width + x] = pixel;
+ };
+
+ _this.write = function(out) {
+
+ //---------------------------------
+ // GIF Signature
+
+ out.writeString('GIF87a');
+
+ //---------------------------------
+ // Screen Descriptor
+
+ out.writeShort(_width);
+ out.writeShort(_height);
+
+ out.writeByte(0x80); // 2bit
+ out.writeByte(0);
+ out.writeByte(0);
+
+ //---------------------------------
+ // Global Color Map
+
+ // black
+ out.writeByte(0x00);
+ out.writeByte(0x00);
+ out.writeByte(0x00);
+
+ // white
+ out.writeByte(0xff);
+ out.writeByte(0xff);
+ out.writeByte(0xff);
+
+ //---------------------------------
+ // Image Descriptor
+
+ out.writeString(',');
+ out.writeShort(0);
+ out.writeShort(0);
+ out.writeShort(_width);
+ out.writeShort(_height);
+ out.writeByte(0);
+
+ //---------------------------------
+ // Local Color Map
+
+ //---------------------------------
+ // Raster Data
+
+ var lzwMinCodeSize = 2;
+ var raster = getLZWRaster(lzwMinCodeSize);
+
+ out.writeByte(lzwMinCodeSize);
+
+ var offset = 0;
+
+ while (raster.length - offset > 255) {
+ out.writeByte(255);
+ out.writeBytes(raster, offset, 255);
+ offset += 255;
+ }
+
+ out.writeByte(raster.length - offset);
+ out.writeBytes(raster, offset, raster.length - offset);
+ out.writeByte(0x00);
+
+ //---------------------------------
+ // GIF Terminator
+ out.writeString(';');
+ };
+
+ var bitOutputStream = function(out) {
+
+ var _out = out;
+ var _bitLength = 0;
+ var _bitBuffer = 0;
+
+ var _this = {};
+
+ _this.write = function(data, length) {
+
+ if ( (data >>> length) != 0) {
+ throw 'length over';
+ }
+
+ while (_bitLength + length >= 8) {
+ _out.writeByte(0xff & ( (data << _bitLength) | _bitBuffer) );
+ length -= (8 - _bitLength);
+ data >>>= (8 - _bitLength);
+ _bitBuffer = 0;
+ _bitLength = 0;
+ }
+
+ _bitBuffer = (data << _bitLength) | _bitBuffer;
+ _bitLength = _bitLength + length;
+ };
+
+ _this.flush = function() {
+ if (_bitLength > 0) {
+ _out.writeByte(_bitBuffer);
+ }
+ };
+
+ return _this;
+ };
+
+ var getLZWRaster = function(lzwMinCodeSize) {
+
+ var clearCode = 1 << lzwMinCodeSize;
+ var endCode = (1 << lzwMinCodeSize) + 1;
+ var bitLength = lzwMinCodeSize + 1;
+
+ // Setup LZWTable
+ var table = lzwTable();
+
+ for (var i = 0; i < clearCode; i += 1) {
+ table.add(String.fromCharCode(i) );
+ }
+ table.add(String.fromCharCode(clearCode) );
+ table.add(String.fromCharCode(endCode) );
+
+ var byteOut = byteArrayOutputStream();
+ var bitOut = bitOutputStream(byteOut);
+
+ // clear code
+ bitOut.write(clearCode, bitLength);
+
+ var dataIndex = 0;
+
+ var s = String.fromCharCode(_data[dataIndex]);
+ dataIndex += 1;
+
+ while (dataIndex < _data.length) {
+
+ var c = String.fromCharCode(_data[dataIndex]);
+ dataIndex += 1;
+
+ if (table.contains(s + c) ) {
+
+ s = s + c;
+
+ } else {
+
+ bitOut.write(table.indexOf(s), bitLength);
+
+ if (table.size() < 0xfff) {
+
+ if (table.size() == (1 << bitLength) ) {
+ bitLength += 1;
+ }
+
+ table.add(s + c);
+ }
+
+ s = c;
+ }
+ }
+
+ bitOut.write(table.indexOf(s), bitLength);
+
+ // end code
+ bitOut.write(endCode, bitLength);
+
+ bitOut.flush();
+
+ return byteOut.toByteArray();
+ };
+
+ var lzwTable = function() {
+
+ var _map = {};
+ var _size = 0;
+
+ var _this = {};
+
+ _this.add = function(key) {
+ if (_this.contains(key) ) {
+ throw 'dup key:' + key;
+ }
+ _map[key] = _size;
+ _size += 1;
+ };
+
+ _this.size = function() {
+ return _size;
+ };
+
+ _this.indexOf = function(key) {
+ return _map[key];
+ };
+
+ _this.contains = function(key) {
+ return typeof _map[key] != 'undefined';
+ };
+
+ return _this;
+ };
+
+ return _this;
+ };
+
+ var createDataURL = function(width, height, getPixel) {
+ var gif = gifImage(width, height);
+ for (var y = 0; y < height; y += 1) {
+ for (var x = 0; x < width; x += 1) {
+ gif.setPixel(x, y, getPixel(x, y) );
+ }
+ }
+
+ var b = byteArrayOutputStream();
+ gif.write(b);
+
+ var base64 = base64EncodeOutputStream();
+ var bytes = b.toByteArray();
+ for (var i = 0; i < bytes.length; i += 1) {
+ base64.writeByte(bytes[i]);
+ }
+ base64.flush();
+
+ return 'data:image/gif;base64,' + base64;
+ };
+
+ //---------------------------------------------------------------------
+ // returns qrcode function.
+
+ return qrcode;
+}();
+
+// multibyte support
+!function() {
+
+ qrcode.stringToBytesFuncs['UTF-8'] = function(s) {
+ // http://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array
+ function toUTF8Array(str) {
+ var utf8 = [];
+ for (var i=0; i < str.length; i++) {
+ var charcode = str.charCodeAt(i);
+ if (charcode < 0x80) utf8.push(charcode);
+ else if (charcode < 0x800) {
+ utf8.push(0xc0 | (charcode >> 6),
+ 0x80 | (charcode & 0x3f));
+ }
+ else if (charcode < 0xd800 || charcode >= 0xe000) {
+ utf8.push(0xe0 | (charcode >> 12),
+ 0x80 | ((charcode>>6) & 0x3f),
+ 0x80 | (charcode & 0x3f));
+ }
+ // surrogate pair
+ else {
+ i++;
+ // UTF-16 encodes 0x10000-0x10FFFF by
+ // subtracting 0x10000 and splitting the
+ // 20 bits of 0x0-0xFFFFF into two halves
+ charcode = 0x10000 + (((charcode & 0x3ff)<<10)
+ | (str.charCodeAt(i) & 0x3ff));
+ utf8.push(0xf0 | (charcode >>18),
+ 0x80 | ((charcode>>12) & 0x3f),
+ 0x80 | ((charcode>>6) & 0x3f),
+ 0x80 | (charcode & 0x3f));
+ }
+ }
+ return utf8;
+ }
+ return toUTF8Array(s);
+ };
+
+}();
+
+(function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ define([], factory);
+ } else if (typeof exports === 'object') {
+ module.exports = factory();
+ }
+}(function () {
+ return qrcode;
+}));