This commit is contained in:
Kevin McIntyre
2025-06-18 01:00:00 -04:00
commit f84b511895
228 changed files with 42509 additions and 0 deletions

14
jdenticon-js/.eslintrc.js Normal file
View File

@@ -0,0 +1,14 @@
module.exports = {
"env": {
"browser": true,
"es2020": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"rules": {
}
};

3
jdenticon-js/.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
# Treat minifyed files as binary to ensure the integrity does not change
dist/*.min.js binary

View File

@@ -0,0 +1,143 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Tests
on: [push]
env:
TAP_COLORS: 1
jobs:
build:
name: Build and run unit tests
runs-on: ubuntu-latest
env:
TAP_NO_ESM: 1
steps:
- uses: actions/checkout@v4.1.5
- name: Use Node.js v14
uses: actions/setup-node@v4.0.2
with:
node-version: 14.x
- run: npm install
- name: Build Jdenticon
run: npm run build
- name: TypeScript typings tests
run: npm run test:types
- name: Unit tests
run: npm run test:unit
- name: Webpack 4 bundle test
run: npm run test:webpack4
- name: Webpack 5 bundle test
run: npm run test:webpack5
- name: Rollup bundle test
run: npm run test:rollup
- name: Node test (CommonJS)
run: npm run test:node-cjs
- name: Node test (ESM)
run: npm run test:node-esm
- name: Publish artifacts
uses: actions/upload-artifact@v4.3.3
if: ${{ always() }}
with:
name: package
path: ./test/node_modules/jdenticon
e2e:
name: E2E tests (Node ${{ matrix.node }})
runs-on: ubuntu-latest
needs: build
strategy:
fail-fast: false
matrix:
node: [ '6.4', '8.x', '10.x', '12.x', '18.x', '20.x' ]
steps:
- uses: actions/checkout@v4.1.5
- uses: actions/download-artifact@v4.1.7
with:
name: package
path: test/node_modules/jdenticon
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v4.0.2
with:
node-version: ${{ matrix.node }}
# Use an older tap version to ensure compatibility with the old Node version
# bind-obj-methods broke old Node 6 support in 2.0.1
- name: npm install (Node 6.4)
if: ${{ matrix.node == '6.4' }}
run: |
npm install -g npm@6.14.17
npm install tap@12.7.0 bind-obj-methods@2.0.0
- name: npm install (Node 8.x)
if: ${{ matrix.node == '8.x' }}
run: npm install tap@14.11.0
- name: npm install (Node 10+)
if: ${{ matrix.node != '6.4' && matrix.node != '8.x' }}
run: npm install
- name: Node test (CommonJS)
run: npm run test:node-cjs
- name: Node test (ESM, Node 12+)
if: ${{ matrix.node != '6.4' && matrix.node != '8.x' && matrix.node != '10.x' }}
run: npm run test:node-esm
- name: Publish artifacts
uses: actions/upload-artifact@v4.3.3
if: ${{ failure() }}
with:
name: e2e-${{ matrix.node }}
path: ./test/e2e/node/expected
visual:
name: Visual tests
needs: build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ 'macos-latest', 'windows-latest' ]
env:
ARTIFACTS_DIR: ./artifacts
BROWSER_SCREENSHOT_DIR: ./artifacts/screenshots
BROWSER_DIFF_DIR: ./artifacts/diffs
steps:
- uses: actions/checkout@v4.1.5
- name: Use Node.js
uses: actions/setup-node@v4.0.2
with:
node-version: 16.x
- run: npm install
- uses: actions/download-artifact@v4.1.7
with:
name: package
path: test/node_modules/jdenticon
- name: Run visual tests (Windows)
if: ${{ startsWith(matrix.os, 'windows') }}
run: |
$env:PATH = "C:\SeleniumWebDrivers\IEDriver;$env:PATH"
npm run test:browser-win
- name: Run visual tests (macOS)
if: ${{ startsWith(matrix.os, 'macos') }}
run: npm run test:browser-macos
- name: Publish artifacts
uses: actions/upload-artifact@v4.3.3
if: ${{ always() }}
with:
name: visual-${{ matrix.os }}
path: ${{ env.ARTIFACTS_DIR }}

7
jdenticon-js/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
artifacts
obj
releases
bower_components
node_modules
.vs
.nyc_output

21
jdenticon-js/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2014-2024 Daniel Mester Pirttijärvi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

72
jdenticon-js/README.md Normal file
View File

@@ -0,0 +1,72 @@
# [Jdenticon](https://jdenticon.com)
JavaScript library for generating highly recognizable identicons using HTML5 canvas or SVG.
![Sample identicons](https://jdenticon.com/hosted/github-samples.png)
[![Tests](https://img.shields.io/github/actions/workflow/status/dmester/jdenticon/tests.js.yml?branch=master&style=flat-square)](https://github.com/dmester/jdenticon/actions)
[![Downloads](https://img.shields.io/npm/dt/jdenticon.svg?style=flat-square)](https://www.npmjs.com/package/jdenticon)
[![jsDelivr](https://data.jsdelivr.com/v1/package/npm/jdenticon/badge?style=square)](https://www.jsdelivr.com/package/npm/jdenticon)
[![npm bundle size](https://img.shields.io/bundlephobia/min/jdenticon.svg?style=flat-square)](https://bundlephobia.com/result?p=jdenticon)
[![License MIT](https://img.shields.io/badge/license-MIT-green.svg?style=flat-square)](https://github.com/dmester/jdenticon/blob/master/LICENSE)
## Live demo
https://jdenticon.com
## Getting started
Using Jdenticon is simple. Follow the steps below to integrate Jdenticon into your website.
### 1. Add identicon placeholders
Jdenticon is able to render both raster and vector identicons. Raster icons are rendered
slightly faster than vector icons, but vector icons scale better on high resolution screens.
Add a canvas to render a raster icon, or an inline svg element to render a vector icon.
```HTML
<!-- Vector icon -->
<svg width="80" height="80" data-jdenticon-value="icon value"></svg>
<!-- OR -->
<!-- Raster icon -->
<canvas width="80" height="80" data-jdenticon-value="icon value"></canvas>
```
### 2. Add reference to Jdenticon
Include the Jdenticon library somewhere on your page. You can either host it yourself or
use it right off [jsDelivr](https://www.jsdelivr.com).
```HTML
<!-- Using jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/jdenticon@3.3.0/dist/jdenticon.min.js"
integrity="sha384-LfouGM03m83ArVtne1JPk926e3SGD0Tz8XHtW2OKGsgeBU/UfR0Fa8eX+UlwSSAZ"
crossorigin="anonymous">
</script>
<!-- OR -->
<!-- Hosting it yourself -->
<script src="-path-to-/jdenticon.min.js"></script>
```
That's it!
## Other resources
### API documentation
For more usage examples and API documentation, please see:
https://jdenticon.com
### Other platforms
There are ports or bindings for Jdenticon available for the following platforms:
* [PHP](https://github.com/dmester/jdenticon-php/)
* [React](https://www.npmjs.com/package/react-jdenticon)
* [Angular](https://www.npmjs.com/package/ngx-jdenticon)
* [.NET](https://github.com/dmester/jdenticon-net/)
* [Rust](https://github.com/jay3332/rdenticon)
* [Polymer](https://github.com/GeoloeG/identicon-element)
* [Swift](https://github.com/aleph7/jdenticon-swift)
* [Java](https://github.com/sunshower-io/sunshower-arcus/tree/master/arcus-identicon)
* [Dart/Flutter](https://pub.dartlang.org/packages/jdenticon_dart)
* [Kotlin](https://github.com/WycliffeAssociates/jdenticon-kotlin)
## License
Jdenticon is available under the [MIT license](https://github.com/dmester/jdenticon/blob/master/LICENSE).

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env node
const fs = require("fs");
const jdenticon = require("../dist/jdenticon-node");
// Handle command
const parsedArgs = parseArgs(process.argv);
if (parsedArgs.help) {
writeHelp();
process.exit(0);
} else if (parsedArgs.version) {
console.log(jdenticon.version);
process.exit(0);
} else {
const validatedArgs = validateArgs(parsedArgs);
if (validatedArgs) {
var output = validatedArgs.svg ?
jdenticon.toSvg(validatedArgs.value, validatedArgs.size, validatedArgs.config) :
jdenticon.toPng(validatedArgs.value, validatedArgs.size, validatedArgs.config);
if (validatedArgs.output) {
fs.writeFileSync(validatedArgs.output, output);
} else {
process.stdout.write(output);
}
process.exit(0);
} else {
writeHelp();
process.exit(1);
}
}
// Functions
function writeHelp() {
console.log("Generates an identicon as a PNG or SVG file for a specified value.");
console.log("");
console.log("Usage: jdenticon <value> [-s <size>] [-o <filename>]");
console.log("");
console.log("Options:");
console.log(" -s, --size <value> Icon size in pixels. (default: 100)");
console.log(" -o, --output <path> Output file. (default: <stdout>)");
console.log(" -f, --format <svg|png> Format of generated icon. Otherwise detected from output path. (default: png)");
console.log(" -b, --back-color <value> Background color on format #rgb, #rgba, #rrggbb or #rrggbbaa. (default: transparent)");
console.log(" -p, --padding <value> Padding in percent in range 0 to 0.5. (default: 0.08)");
console.log(" -v, --version Gets the version of Jdenticon.");
console.log(" -h, --help Show this help information.");
console.log("");
console.log("Examples:");
console.log(" jdenticon user127 -s 100 -o icon.png");
}
function parseArgs(args) {
// Argument 1 is always node
// Argument 2 is always jdenticon
// Argument 3 and forward are actual arguments
args = args.slice(2);
function consume(aliases, hasValue) {
for (var argIndex = 0; argIndex < args.length; argIndex++) {
var arg = args[argIndex];
for (var aliasIndex = 0; aliasIndex < aliases.length; aliasIndex++) {
var alias = aliases[aliasIndex];
if (arg === alias) {
var value;
if (hasValue) {
if (argIndex + 1 < args.length) {
value = args[argIndex + 1];
} else {
console.warn("WARN Missing value of argument " + alias);
}
} else {
value = true;
}
args.splice(argIndex, hasValue ? 2 : 1);
return value;
}
if (arg.startsWith(alias) && arg[alias.length] === "=") {
var value = arg.substr(alias.length + 1);
if (!hasValue) {
value = value !== "false";
}
args.splice(argIndex, 1);
return value;
}
}
}
}
if (consume(["-h", "--help", "-?", "/?", "/h"], false)) {
return {
help: true
};
}
if (consume(["-v", "--version"], false)) {
return {
version: true
};
}
return {
size: consume(["-s", "--size"], true),
output: consume(["-o", "--output"], true),
format: consume(["-f", "--format"], true),
padding: consume(["-p", "--padding"], true),
backColor: consume(["-b", "--back-color"], true),
value: args
};
}
function validateArgs(args) {
if (args.value.length) {
// Size
var size = 100;
if (args.size) {
size = Number(args.size);
if (!size || size < 1) {
size = 100;
console.warn("WARN Invalid size specified. Defaults to 100.");
}
}
// Padding
var padding;
if (args.padding != null) {
padding = Number(args.padding);
if (isNaN(padding) || padding < 0 || padding >= 0.5) {
padding = 0.08;
console.warn("WARN Invalid padding specified. Defaults to 0.08.");
}
}
// Background color
var backColor;
if (args.backColor != null) {
backColor = args.backColor;
if (!/^(#[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(backColor)) {
backColor = undefined;
console.warn("WARN Invalid background color specified. Defaults to transparent.");
}
}
// Format
var generateSvg =
args.format ? /^svg$/i.test(args.format) :
args.output ? /\.svg$/i.test(args.output) :
false;
if (args.format != null && !/^(svg|png)$/i.test(args.format)) {
console.warn("WARN Invalid format specified. Defaults to " + (generateSvg ? "svg" : "png") + ".");
}
return {
config: {
padding: padding,
backColor: backColor
},
output: args.output,
size: size,
svg: generateSvg,
value: args.value.join("")
};
}
}

32
jdenticon-js/bower.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "Jdenticon",
"authors": [
"Daniel Mester Pirttijärvi"
],
"description": "Javascript identicon generator",
"main": "dist/jdenticon.js",
"keywords": [
"javascript",
"identicon",
"avatar",
"library"
],
"license": "MIT",
"homepage": "https://jdenticon.com/",
"ignore": [
".npmignore",
".gitignore",
".vs",
"*.bat",
"*.nuspec",
"build",
"gulpfile.js",
"node_modules",
"obj",
"releases",
"src",
"template.*",
"test",
"utils"
]
}

View File

@@ -0,0 +1,4 @@
{
"main": "../dist/jdenticon-module",
"types": "../types/module.d.ts"
}

View File

@@ -0,0 +1,34 @@
const { Transform } = require("stream");
const { parse } = require("acorn");
const { Replacement } = require("./replacement");
function astTransformStream(transformer) {
return new Transform({
objectMode: true,
transform(inputFile, _, fileDone) {
const input = inputFile.contents.toString();
const comments = [];
const ast = parse(input, {
ecmaVersion: 10,
sourceType: "module",
onComment: comments,
});
const replacement = new Replacement();
transformer(replacement, ast, comments, input);
const output = replacement.replace(input, inputFile.sourceMap);
inputFile.contents = Buffer.from(output.output);
if (inputFile.sourceMap) {
inputFile.sourceMap = output.sourceMap;
}
fileDone(null, inputFile);
}
});
}
module.exports = transformer => () => astTransformStream(transformer);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,186 @@
const { Node } = require("acorn");
const astTransformStream = require("./ast-transform-stream");
const DOMPROPS = require("./domprops");
const RESERVED_NAMES = require("./reserved-keywords");
const CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_";
function visit(node, visitor) {
if (node instanceof Node) {
visitor(node);
}
for (const key in node) {
const value = node[key];
if (Array.isArray(value)) {
value.forEach(subNode => visit(subNode, visitor));
} else if (value instanceof Node) {
visit(value, visitor);
}
}
}
function generateIdentifier(seed) {
let identifier = "";
seed = Math.abs(Math.floor(seed));
do {
const mod = seed % CHARACTERS.length;
identifier += CHARACTERS[mod];
seed = (seed / CHARACTERS.length) | 0;
} while (seed);
return identifier;
}
function mangleProps(input, ast, replacement) {
const identifierNodes = [];
const longToShortName = new Map();
// Find identifiers
visit(ast, node => {
let identifier;
if (node.type === "MemberExpression" && !node.computed) {
// Matches x.y
// Not x["y"] (computed: true)
identifier = node.property;
} else if (node.type === "MethodDefinition") {
// Matches x() { }
identifier = node.key;
} else if (node.type === "Property") {
// Matches { x: y }
// Not { "x": y }
identifier = node.key;
}
if (identifier && identifier.type === "Identifier") {
identifierNodes.push(identifier);
}
});
// Collect usage statistics per name
const usageMap = new Map();
identifierNodes.forEach(node => {
if (node.name && !RESERVED_NAMES.has(node.name) && !DOMPROPS.has(node.name)) {
usageMap.set(node.name, (usageMap.get(node.name) || 0) + 1);
}
});
// Sort by usage in descending order
const usageStats = Array.from(usageMap).sort((a, b) => b[1] - a[1]);
// Allocate identifiers in order of usage statistics to ensure
// frequently used symbols get as short identifiers as possible.
let runningCounter = 0;
usageStats.forEach(identifier => {
const longName = identifier[0];
if (!longToShortName.has(longName)) {
let shortName;
do {
shortName = generateIdentifier(runningCounter++);
} while (RESERVED_NAMES.has(shortName) || DOMPROPS.has(shortName));
longToShortName.set(longName, shortName);
}
});
// Populate replacements
identifierNodes.forEach(node => {
const minifiedName = longToShortName.get(node.name);
if (minifiedName) {
replacement.addRange({
start: node.start,
end: node.end,
replacement: minifiedName + "/*" + node.name + "*/",
name: node.name,
});
}
});
return replacement;
}
function simplifyES5Class(input, ast, replacement) {
const prototypeMemberExpressions = [];
const duplicateNamedFunctions = [];
visit(ast, node => {
if (node.type === "MemberExpression" &&
!node.computed &&
node.object.type === "Identifier" &&
node.property.type === "Identifier" &&
node.property.name === "prototype"
) {
// Matches: xxx.prototype
prototypeMemberExpressions.push(node);
} else if (
node.type === "VariableDeclaration" &&
node.declarations.length === 1 &&
node.declarations[0].init &&
node.declarations[0].init.type === "FunctionExpression" &&
node.declarations[0].init.id &&
node.declarations[0].init.id.name === node.declarations[0].id.name
) {
// Matches: var xxx = function xxx ();
duplicateNamedFunctions.push(node);
}
});
duplicateNamedFunctions.forEach(duplicateNamedFunction => {
const functionName = duplicateNamedFunction.declarations[0].init.id.name;
// Remove: var xxx =
replacement.addRange({
start: duplicateNamedFunction.start,
end: duplicateNamedFunction.declarations[0].init.start,
replacement: "",
});
// Remove trailing semicolons
let semicolons = 0;
while (input[duplicateNamedFunction.end - semicolons - 1] === ";") semicolons++;
// Find prototype references
const refs = prototypeMemberExpressions.filter(node => node.object.name === functionName);
if (refs.length > 1) {
// Insert: var xx__prototype = xxx.prototype;
replacement.addRange({
start: duplicateNamedFunction.end - semicolons,
end: duplicateNamedFunction.end,
replacement: `\r\nvar ${functionName}__prototype = ${functionName}.prototype;`,
});
// Replace references
refs.forEach(ref => {
replacement.addRange({
start: ref.start,
end: ref.end,
replacement: `${functionName}__prototype`,
});
});
} else if (semicolons) {
replacement.addRange({
start: duplicateNamedFunction.end - semicolons,
end: duplicateNamedFunction.end,
replacement: "",
});
}
});
}
function browserConstants(input, ast, replacement) {
replacement.addText("Node.ELEMENT_NODE", "1");
}
const MINIFIERS = [simplifyES5Class, mangleProps, browserConstants];
module.exports = astTransformStream(function (replacement, ast, comments, input) {
MINIFIERS.forEach(minifier => minifier(input, ast, replacement));
});

View File

@@ -0,0 +1,52 @@
const astTransformStream = require("./ast-transform-stream");
function removeJSDocImports(comments, replacement) {
const REGEX = /[ \t]*\*[ \t]*@typedef\s+\{import.+\r?\n?|import\(.*?\)\.([a-zA-Z_]+)/g;
const JSDOC_COMMENT_OFFSET = 2;
comments.forEach(comment => {
if (comment.type === "Block" && comment.value[0] === "*") {
// JSDoc comment
const resultingComment = comment.value.replace(REGEX, (match, importName, matchIndex) => {
matchIndex += comment.start + JSDOC_COMMENT_OFFSET;
if (importName) {
// { import().xxx }
replacement.addRange({
start: matchIndex,
end: matchIndex + match.length,
replacement: importName,
});
return importName;
} else {
// @typedef
replacement.addRange({
start: matchIndex,
end: matchIndex + match.length,
replacement: "",
});
return "";
}
});
if (!/[^\s\*]/.test(resultingComment)) {
// Empty comment left
replacement.addRange({
start: comment.start,
end: comment.end,
replacement: "",
});
}
}
});
}
module.exports = astTransformStream(function (replacement, ast, comments, input) {
removeJSDocImports(comments, replacement);
});

View File

@@ -0,0 +1,54 @@
const { Transform } = require("stream");
const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
function removeMappedSourceStream(...sourceNames) {
const sourceNamesToRemove = new Set(sourceNames);
return new Transform({
objectMode: true,
transform(inputFile, _, fileDone) {
if (inputFile.sourceMap) {
let consumer = inputFile.sourceMap;
if (!(consumer instanceof SourceMapConsumer)) {
consumer = new SourceMapConsumer(consumer);
}
const generator = new SourceMapGenerator({
file: consumer.file,
sourceRoot: consumer.sourceRoot,
});
consumer.sources.forEach(sourceFile => {
const content = consumer.sourceContentFor(sourceFile);
if (content != null && !sourceNamesToRemove.has(sourceFile)) {
generator.setSourceContent(sourceFile, content);
}
});
consumer.eachMapping(mapping => {
if (!sourceNamesToRemove.has(mapping.source)) {
generator.addMapping({
original: {
line: mapping.originalLine,
column: mapping.originalColumn,
},
generated: {
line: mapping.generatedLine,
column: mapping.generatedColumn,
},
source: mapping.source,
name: mapping.name,
});
}
});
inputFile.sourceMap = generator.toJSON();
}
fileDone(null, inputFile);
}
});
}
module.exports = removeMappedSourceStream;

View File

@@ -0,0 +1,672 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
const { Transform } = require("stream");
const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
/**
* Finds substrings and replaces them with other strings, keeping any input source map up-to-date.
*
* @example
* const replacement = new Replacement([
* ["find this", "replace with this"],
* [/find this/gi, "replace with this"]
* ]);
* replacement.replace(input, inputSourceMap);
*
* @example
* const replacement = new Replacement("find this", "replace with this");
* replacement.replace(input, inputSourceMap);
*/
class Replacement {
constructor(...definition) {
/**
* @type {function(Array<OverwriteRange>, string): void}
*/
this._matchers = [];
/**
* @type {Array<OverwriteRange>}
*/
this._ranges = [];
this.add(definition);
}
/**
* @param {string} input
* @returns {Array<OverwriteRange>}
*/
matchAll(input) {
const ranges = [...this._ranges];
this._matchers.forEach(matcher => matcher(ranges, input));
let lastReplacement;
return ranges
.sort((a, b) => a.start - b.start)
.filter(replacement => {
if (!lastReplacement || lastReplacement.end <= replacement.start) {
lastReplacement = replacement;
return true;
}
});
}
/**
* @param {string} input
* @param {SourceMap=} inputSourceMap
* @returns {{ output: string, sourceMap: SourceMap }}
*/
replace(input, inputSourceMap) {
const ranges = this.matchAll(input);
const offset = new Offset();
const reader = new InputReader(input);
const sourceMap = new SourceMapSpooler(inputSourceMap);
const output = [];
if (sourceMap.isEmpty()) {
sourceMap.initEmpty(reader.lines);
}
ranges.forEach((range, rangeIndex) => {
output.push(reader.readTo(range.start));
output.push(range.replacement);
const inputStart = reader.pos;
const replacedText = reader.readTo(range.end);
if (replacedText === range.replacement) {
return; // Nothing to do
}
const inputEnd = reader.pos;
const replacementLines = range.replacement.split(/\n/g);
const lineDifference = replacementLines.length + inputStart.line - inputEnd.line - 1;
const outputStart = {
line: inputStart.line + offset.lineOffset,
column: inputStart.column + offset.getColumnOffset(inputStart.line)
};
const outputEnd = {
line: inputEnd.line + offset.lineOffset + lineDifference,
column: replacementLines.length > 1 ?
replacementLines[replacementLines.length - 1].length :
inputStart.column + offset.getColumnOffset(inputStart.line) +
range.replacement.length
}
sourceMap.spoolTo(inputStart.line, inputStart.column, offset);
offset.lineOffset += lineDifference;
offset.setColumnOffset(inputEnd.line, outputEnd.column - inputEnd.column);
if (range.name || replacementLines.length === 1 && range.replacement) {
const mappingBeforeStart = sourceMap.lastMapping();
const mappingAfterStart = sourceMap.nextMapping();
if (mappingAfterStart &&
mappingAfterStart.generatedLine === inputStart.line &&
mappingAfterStart.generatedColumn === inputStart.column
) {
sourceMap.addMapping({
original: {
line: mappingAfterStart.originalLine,
column: mappingAfterStart.originalColumn,
},
generated: {
line: outputStart.line,
column: outputStart.column
},
source: mappingAfterStart.source,
name: range.name,
});
} else if (mappingBeforeStart && mappingBeforeStart.generatedLine === inputStart.line) {
sourceMap.addMapping({
original: {
line: mappingBeforeStart.originalLine + inputStart.line - mappingBeforeStart.generatedLine,
column: mappingBeforeStart.originalColumn + inputStart.column - mappingBeforeStart.generatedColumn,
},
generated: {
line: outputStart.line,
column: outputStart.column
},
source: mappingBeforeStart.source,
name: range.name,
});
}
} else if (range.replacement) {
// Map longer replacements to a virtual file defined in the source map
const generatedSourceName = sourceMap.addSourceContent(replacedText, range.replacement);
for (var i = 0; i < replacementLines.length; i++) {
// Don't map empty lines
if (replacementLines[i]) {
sourceMap.addMapping({
original: {
line: i + 1,
column: 0,
},
generated: {
line: outputStart.line + i,
column: i ? 0 : outputStart.column,
},
source: generatedSourceName,
});
}
}
}
sourceMap.skipTo(inputEnd.line, inputEnd.column, offset);
// Add a source map node directly after the replacement to terminate the replacement
const mappingAfterEnd = sourceMap.nextMapping();
const mappingBeforeEnd = sourceMap.lastMapping();
if (mappingAfterEnd &&
mappingAfterEnd.generatedLine === inputEnd.line &&
mappingAfterEnd.generatedColumn === inputEnd.column
) {
// No extra source map node needed when the replacement is directly followed by another node
} else if (rangeIndex + 1 < ranges.length && range.end === ranges[rangeIndex + 1].start) {
// The next replacement range is adjacent to this one
} else if (reader.endOfLine()) {
// End of line, no point in adding a following node
} else if (!mappingBeforeEnd || mappingBeforeEnd.generatedLine !== inputEnd.line) {
// No applicable preceding node found
} else {
sourceMap.addMapping({
original: {
line: mappingBeforeEnd.originalLine + inputEnd.line - mappingBeforeEnd.generatedLine,
column: mappingBeforeEnd.originalColumn + inputEnd.column - mappingBeforeEnd.generatedColumn,
},
generated: {
line: outputEnd.line,
column: outputEnd.column
},
source: mappingBeforeEnd.source,
});
}
});
// Flush remaining input to output and source map
output.push(reader.readToEnd());
sourceMap.spoolToEnd(offset);
return {
output: output.join(""),
sourceMap: sourceMap.toJSON(),
};
}
add(value) {
const target = this;
function addRecursive(innerValue) {
if (innerValue != null) {
if (Array.isArray(innerValue)) {
const needle = innerValue[0];
if (typeof needle === "string") {
target.addText(...innerValue);
} else if (needle instanceof RegExp) {
target.addRegExp(...innerValue);
} else {
innerValue.forEach(addRecursive);
}
} else if (innerValue instanceof Replacement) {
target._matchers.push(innerValue._matchers);
target._ranges.push(innerValue._ranges);
} else if (typeof innerValue === "object") {
target.addRange(innerValue);
} else {
throw new Error("Unknown replacement argument specified.");
}
}
}
addRecursive(value);
}
/**
* @param {RegExp} re
* @param {string|function(string, ...):string} replacement
* @param {{ name: string }=} rangeOpts
*/
addRegExp(re, replacement, rangeOpts) {
const replacementFactory = this._createReplacementFactory(replacement);
this._matchers.push((ranges, input) => {
const isGlobalRegExp = /g/.test(re.flags);
let match;
let isFirstIteration = true;
while ((isFirstIteration || isGlobalRegExp) && (match = re.exec(input))) {
ranges.push(new OverwriteRange({
...rangeOpts,
start: match.index,
end: match.index + match[0].length,
replacement: replacementFactory(match, match.index, input),
}));
isFirstIteration = false;
}
});
}
/**
* @param {string} needle
* @param {string|function(string, ...):string} replacement
* @param {{ name: string }=} rangeOpts
*/
addText(needle, replacement, rangeOpts) {
const replacementFactory = this._createReplacementFactory(replacement);
this._matchers.push((ranges, input) => {
let index = -needle.length;
while ((index = input.indexOf(needle, index + needle.length)) >= 0) {
ranges.push(new OverwriteRange({
...rangeOpts,
start: index,
end: index + needle.length,
replacement: replacementFactory([needle], index, input),
}));
}
});
}
/**
* @param {OverwriteRange} range
*/
addRange(range) {
this._ranges.push(new OverwriteRange(range));
}
/**
* @param {string|function(string, ...):string} replacement
* @returns {function(Array<string>, number, string):string}
*/
_createReplacementFactory(replacement) {
if (typeof replacement === "function") {
return (match, index, input) => replacement(...match, index, input);
}
if (replacement == null) {
return () => "";
}
replacement = replacement.toString();
if (replacement.indexOf("$") < 0) {
return () => replacement;
}
return (match, index, input) =>
replacement.replace(/\$(\d+|[$&`'])/g, matchedPattern => {
if (matchedPattern === "$$") {
return "$";
}
if (matchedPattern === "$&") {
return match[0];
}
if (matchedPattern === "$`") {
return input.substring(0, index);
}
if (matchedPattern === "$'") {
return input.substring(index + match[0].length);
}
const matchArrayIndex = Number(matchedPattern.substring(1));
return match[matchArrayIndex];
});
}
}
class InputReader {
/**
* @param {string} input
*/
constructor (input) {
// Find index of all line breaks
const lineBreakIndexes = [];
let index = -1;
while ((index = input.indexOf("\n", index + 1)) >= 0) {
lineBreakIndexes.push(index);
}
this._input = input;
this._inputCursorExclusive = 0;
this._output = [];
this._lineBreakIndexes = lineBreakIndexes;
/**
* Number of lines in the input file.
* @type {number}
*/
this.lines = this._lineBreakIndexes.length + 1;
/**
* Position of the input cursor. Line number is one-based and column number is zero-based.
* @type {{ line: number, column: number }}
*/
this.pos = { line: 1, column: 0 };
}
readTo(exclusiveIndex) {
let result = "";
if (this._inputCursorExclusive < exclusiveIndex) {
result = this._input.substring(this._inputCursorExclusive, exclusiveIndex);
this._inputCursorExclusive = exclusiveIndex;
this._updatePos();
}
return result;
}
readToEnd() {
return this.readTo(this._input.length);
}
endOfLine() {
const nextChar = this._input[this._inputCursorExclusive];
return !nextChar || nextChar === "\r" || nextChar === "\n";
}
_updatePos() {
let line = this.pos.line;
while (
line - 1 < this._lineBreakIndexes.length &&
this._lineBreakIndexes[line - 1] < this._inputCursorExclusive
) {
line++;
}
const lineStartIndex = this._lineBreakIndexes[line - 2];
const column = this._inputCursorExclusive - (lineStartIndex || -1) - 1;
this.pos = { line, column };
}
}
class SourceMapSpooler {
/**
* @param {SourceMap=} inputSourceMap
*/
constructor(inputSourceMap) {
let generator;
let file;
let sources;
let mappings = [];
if (inputSourceMap) {
if (!(inputSourceMap instanceof SourceMapConsumer)) {
inputSourceMap = new SourceMapConsumer(inputSourceMap);
}
generator = new SourceMapGenerator({
file: inputSourceMap.file,
sourceRoot: inputSourceMap.sourceRoot,
});
inputSourceMap.sources.forEach(function(sourceFile) {
const content = inputSourceMap.sourceContentFor(sourceFile);
if (content != null) {
generator.setSourceContent(sourceFile, content);
}
});
inputSourceMap.eachMapping(mapping => {
mappings.push(mapping);
});
mappings.sort((a, b) => a.generatedLine == b.generatedLine
? a.generatedColumn - b.generatedColumn : a.generatedLine - b.generatedLine);
file = inputSourceMap.file;
sources = inputSourceMap.sources;
} else {
generator = new SourceMapGenerator();
file = "input";
sources = [];
}
this._generator = generator;
this._sources = new Set(sources);
this._file = file;
this._mappingsCursor = 0;
this._mappings = mappings;
this._contents = new Map();
}
lastMapping() {
return this._mappings[this._mappingsCursor - 1];
}
nextMapping() {
return this._mappings[this._mappingsCursor];
}
addMapping(mapping) {
this._generator.addMapping(mapping);
}
isEmpty() {
return this._mappings.length === 0;
}
initEmpty(lines) {
this._mappings = [];
for (var i = 0; i < lines; i++) {
this._mappings.push({
originalLine: i + 1,
originalColumn: 0,
generatedLine: i + 1,
generatedColumn: 0,
source: this._file
});
}
}
addSourceContent(replacedText, content) {
let sourceName = this._contents.get(content);
if (!sourceName) {
const PREFIX = "replacement/";
let sourceNameWithoutNumber = PREFIX;
sourceName = sourceNameWithoutNumber + "1";
if (replacedText.length > 0 && replacedText.length < 25) {
replacedText = replacedText
.replace(/^[^0-9a-z-_]+|[^0-9a-z-_]+$/ig, "")
.replace(/\s+/g, "-")
.replace(/[^0-9a-z-_]/ig, "");
if (replacedText) {
sourceNameWithoutNumber = PREFIX + replacedText + "-";
sourceName = PREFIX + replacedText;
}
}
let counter = 2;
while (this._sources.has(sourceName)) {
sourceName = sourceNameWithoutNumber + counter++;
}
this._sources.add(sourceName);
this._contents.set(content, sourceName);
this._generator.setSourceContent(sourceName, content);
}
return sourceName;
}
/**
* Copies source map info from input to output up to but not including the specified position.
* @param {number} line
* @param {number} column
* @param {Offset} offset
*/
spoolTo(line, column, offset) {
this._consume(line, column, offset, true);
}
/**
* Copies remaining source map info from input to output.
* @param {number} line
* @param {number} column
* @param {Offset} offset
*/
spoolToEnd(offset) {
this._consume(null, null, offset, true);
}
/**
* Discards source map info from input up to but not including the specified position.
* @param {number} line
* @param {number} column
* @param {Offset} offset
*/
skipTo(line, column, offset) {
this._consume(line, column, offset, false);
}
toJSON() {
return this._generator.toJSON();
}
_consume(line, column, offset, keep) {
let mapping;
while (
(mapping = this._mappings[this._mappingsCursor]) &&
(
line == null ||
mapping.generatedLine < line ||
mapping.generatedLine == line && mapping.generatedColumn < column
)
) {
if (keep) {
this._generator.addMapping({
original: {
line: mapping.originalLine,
column: mapping.originalColumn,
},
generated: {
line: mapping.generatedLine + offset.lineOffset,
column: mapping.generatedColumn + offset.getColumnOffset(mapping.generatedLine),
},
source: mapping.source,
name: mapping.name,
});
}
this._mappingsCursor++;
}
}
}
class Offset {
constructor() {
this.lineOffset = 0;
this._columnOffset = 0;
this._columnOffsetForLine = 0;
}
setColumnOffset(lineNumber, columnOffset) {
this._columnOffsetForLine = lineNumber;
this._columnOffset = columnOffset;
}
getColumnOffset(lineNumber) {
return this._columnOffsetForLine === lineNumber ?
this._columnOffset : 0;
}
}
class OverwriteRange {
constructor(options) {
if (!isFinite(options.start)) {
throw new Error("A replacement start index is required.");
}
if (!isFinite(options.end)) {
throw new Error("A replacement end index is required.");
}
if (options.end < options.start) {
throw new Error("Replacement end index cannot precede its start index.");
}
/**
* Inclusive start index.
* @type {number}
*/
this.start = options.start;
/**
* Exclusive start index.
* @type {number}
*/
this.end = options.end;
/**
* The replacement interval will be replaced with this string.
* @type string
*/
this.replacement = "" + options.replacement;
/**
* Optional name that will be mapped in the source map.
* @type string
*/
this.name = options.name;
}
}
function gulp(replacements) {
if (typeof replacements === "string" || replacements instanceof RegExp) {
replacements = Array.from(arguments);
}
const replacer = new Replacement(replacements);
return new Transform({
objectMode: true,
transform(inputFile, _, fileDone) {
const input = inputFile.contents.toString();
const output = replacer.replace(input, inputFile.sourceMap);
inputFile.contents = Buffer.from(output.output);
if (inputFile.sourceMap) {
inputFile.sourceMap = output.sourceMap;
}
fileDone(null, inputFile);
}
});
}
module.exports = { Replacement, gulp };

View File

@@ -0,0 +1,70 @@
module.exports = new Set([
"async",
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"export",
"extends",
"finally",
"for",
"function",
"if",
"import",
"in",
"of",
"instanceof",
"new",
"return",
"super",
"switch",
"this",
"throw",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
"enum",
"implements",
"interface",
"let",
"package",
"private",
"protected",
"public",
"static",
"await",
"abstract",
"boolean",
"byte",
"char",
"double",
"final",
"float",
"goto",
"int",
"long",
"native",
"short",
"synchronized",
"throws",
"transient",
"volatile",
"arguments",
"get",
"set",
"null",
"undefined",
"exports",
"module",
]);

View File

@@ -0,0 +1,56 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
const { rollup } = require("rollup");
const Vinyl = require("vinyl");
const applySourceMap = require("vinyl-sourcemaps-apply");
const { Transform } = require("stream");
function rollupStream(options) {
return new Transform({
objectMode: true,
transform(inputFile, _, fileDone) {
const inputOptions = {
onwarn: warn => console.warn(warn.toString()),
...options,
input: inputFile.path,
};
delete inputOptions.output;
rollup(inputOptions).then(bundle => {
return bundle.generate({
...options.output,
sourcemap: !!inputFile.sourceMap
});
}).then(outputs => {
for (const output of outputs.output) {
if (output.type === "chunk") {
const outputFile = new Vinyl({
cwd: inputFile.cwd,
base: inputFile.base,
path: inputFile.path,
contents: Buffer.from(output.code),
});
if (inputFile.sourceMap) {
applySourceMap(outputFile, output.map);
}
this.push(outputFile);
}
}
fileDone();
}, err => fileDone(err));
}
});
}
module.exports = rollupStream;

View File

@@ -0,0 +1,26 @@
const fs = require("fs");
const replace = require("./replacement").gulp;
function wrapTemplate(templatePath, variables) {
let template = fs.readFileSync(templatePath).toString();
if (variables) {
variables.forEach(variable => {
template = template.replace(variable[0], variable[1]);
});
}
template = template.split(/\/\*content\*\//);
const replacements = [];
if (template[0]) {
replacements.push([/^/, template[0]]);
}
if (template[1]) {
replacements.push([/$/, template[1]]);
}
return replace(replacements);
}
module.exports = wrapTemplate;

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Jdenticon</id>
<version>#version#</version>
<title>Jdenticon JS</title>
<authors>Daniel Mester Pirttijärvi</authors>
<license type="expression">MIT</license>
<projectUrl>http://jdenticon.com/</projectUrl>
<iconUrl>http://jdenticon.com/hosted/nuget-logo-js.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>JavaScript library generating identicons using SVG graphics or HTML5 canvas.
If you intend to generate icons server-side, consider using any of the .NET packages instead:
* Jdenticon-net
* Jdenticon.AspNetCore
* Jdenticon.AspNet.Mvc
* Jdenticon.AspNet.WebApi
* Jdenticon.AspNet.WebForms</description>
<tags>JavaScript identicon avatar library</tags>
</metadata>
<files>
<file src="obj\output\jdenticon-#version#.min.js" target="Content\Scripts\jdenticon-#version#.min.js" />
<file src="obj\output\jdenticon-#version#.js" target="Content\Scripts\jdenticon-#version#.js" />
<file src="obj\output\readme.txt" target="Content\Scripts\jdenticon.readme.txt" />
</files>
</package>

Binary file not shown.

View File

@@ -0,0 +1,70 @@
Apache License 2.0 (Apache)
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
1. You must give any other recipients of the Work or Derivative Works a copy of this License; and
2. You must cause any modified files to carry prominent notices stating that You changed the files; and
3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.

2
jdenticon-js/build/template-min.js vendored Normal file
View File

@@ -0,0 +1,2 @@
// Jdenticon #version# | jdenticon.com | MIT licensed | (c) 2014-#year# Daniel Mester Pirttijärvi
/*content*/

View File

@@ -0,0 +1,10 @@
/**
* Jdenticon #version#
* http://jdenticon.com
*
* Built: #date#
*
* #license#
*/
/*content*/

View File

@@ -0,0 +1,27 @@
/**
* Jdenticon #version#
* http://jdenticon.com
*
* Built: #date#
*
* #license#
*/
(function (umdGlobal, factory) {
var jdenticon = factory(umdGlobal);
// Node.js
if (typeof module !== "undefined" && "exports" in module) {
module["exports"] = jdenticon;
}
// RequireJS
else if (typeof define === "function" && define["amd"]) {
define([], function () { return jdenticon; });
}
// No module loader
else {
umdGlobal["jdenticon"] = jdenticon;
}
})(typeof self !== "undefined" ? self : this, function (umdGlobal) {
/*content*/
});

67
jdenticon-js/dist/README.md vendored Normal file
View File

@@ -0,0 +1,67 @@
# What file should I use?
## Overview
| Platform | Bundle | File name |
|----------|------------------|----------------------|
| Browser | Standalone (UMD) | jdenticon.js |
| | | jdenticon.min.js |
| | ES module | jdenticon-module.mjs |
| | CommonJS module | jdenticon-module.js |
| Node.js | ES module | jdenticon-node.mjs |
| | CommonJS module | jdenticon-node.js |
## Node vs browser
There are separate bundles for Node.js and browsers. The Node.js bundles contain support for generating PNG icons, while the browser bundles have support for updating DOM elements. It is important that the right bundle is used, since a web application bundle will be significally larger if the Node bundle is imported instead of the browser bundle.
## Don't address `dist/*` directly
In first hand, don't import a specific file from the `dist` folder. Instead import the Jdenticon package and let the package decide what file to be imported. If your bundler does not pick the right file, and you cannot configure it to do so, there are explicit exports that you can use to force it to use the correct bundle:
| Platform | Export | Example |
|----------|----------------------|----------------------------------------------|
| Browser | jdenticon/browser | `import { toSvg } from "jdenticon/browser";` |
| Node.js | jdenticon/node | `import { toSvg } from "jdenticon/node";` |
| UMD | jdenticon/standalone | `import "jdenticon/standalone";` |
Jdenticon has multiple public entry points:
### ES module
For browsers `jdenticon-module.mjs` is imported and in Node.js environments `jdenticon-node.mjs` is imported. This is the preferred way of using Jdenticon since your bundler will most likely be able to eliminiate code from Jdenticon not used in your application (a.k.a. tree-shaking).
**Example**
```js
import { toSvg } from "jdenticon";
console.log(toSvg("my value", 100));
```
### CommonJS module
If Jdenticon is imported on platforms not supporting ES modules, `jdenticon-module.js` is imported for browser environments and `jdenticon-node.js` in Node.js environments.
**Example**
```js
const { toSvg } = require("jdenticon");
console.log(toSvg("my value", 100));
// or
const jdenticon = require("jdenticon");
console.log(jdenticon.toSvg("my value", 100));
```
### Standalone browser package
This package will render icons automatically at startup and also provides a legacy jQuery plugin, if jQuery is loaded before Jdenticon.
**Example**
```js
import "jdenticon/standalone";
// or
import { toSvg } from "jdenticon/standalone";
console.log(toSvg("my value", 100));
```

1405
jdenticon-js/dist/jdenticon-module.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1399
jdenticon-js/dist/jdenticon-module.mjs vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1276
jdenticon-js/dist/jdenticon-node.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1240
jdenticon-js/dist/jdenticon-node.mjs vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1507
jdenticon-js/dist/jdenticon.js vendored Normal file

File diff suppressed because it is too large Load Diff

3
jdenticon-js/dist/jdenticon.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

298
jdenticon-js/gulpfile.js Normal file
View File

@@ -0,0 +1,298 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
"use strict";
const del = require("del");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const { exec } = require("child_process");
const { promisify } = require("util");
const pack = require("./package.json");
// Gulp dependencies
const gulp = require("gulp");
const rename = require("gulp-rename");
const terser = require("gulp-terser");
const zip = require("gulp-zip");
const replace = require("./build/gulp/replacement").gulp;
const wrapTemplate = require("./build/gulp/wrap-template");
const buble = require("gulp-buble");
const sourcemaps = require("gulp-sourcemaps");
const preMinify = require("./build/gulp/pre-minify");
const removeJsDocImports = require("./build/gulp/remove-jsdoc-imports");
const removeMappedSource = require("./build/gulp/remove-mapped-source");
// Rollup dependencies
const rollup = require("./build/gulp/rollup");
const commonjs = require( "@rollup/plugin-commonjs");
const stripBanner = require("rollup-plugin-strip-banner");
const alias = require("@rollup/plugin-alias");
// Constants
const LICENSE = fs.readFileSync("./LICENSE").toString();
const VARIABLES = [
[/#version#/g, pack.version],
[/#year#/g, new Date().getFullYear()],
[/#date#/g, new Date().toISOString()],
// Keep line prefix, e.g. " * " for license banners in JavaScript.
[/(.*)#license#/gm, "$1" + LICENSE.trim().replace(/\n/g, "\n$1")],
];
function umdSrc() {
return gulp.src("./src/browser-umd.js")
.pipe(sourcemaps.init())
.pipe(rollup({
output: { format: "cjs" },
plugins: [
stripBanner(),
alias({
entries: [
{ find: /^(.*[\/\\])global$/, replacement: "$1global.umd" },
]
}),
],
}))
.pipe(rename(function (path) { path.basename = "notmapped"; path.extname = ".js" }))
.pipe(buble())
.pipe(preMinify())
.pipe(removeJsDocImports())
// The UMD template expects a factory function body, so replace export with a return for the factory function.
.pipe(replace("module.exports = ", "return "))
.pipe(replace(VARIABLES))
.pipe(wrapTemplate("./build/template-umd.js", VARIABLES));
}
gulp.task("clean", function () {
return del(["./~jdenticon.nuspec", "./obj/output"]);
});
gulp.task("build-umd", function () {
return umdSrc()
.pipe(rename(function (path) { path.basename = "jdenticon"; path.extname = ".js" }))
.pipe(gulp.dest("dist"))
.pipe(rename(function (path) { path.basename = "jdenticon-" + pack.version; path.extname = ".js" }))
.pipe(gulp.dest("obj/output"));
});
gulp.task("build-umd-min", function () {
return umdSrc()
.pipe(terser())
.pipe(wrapTemplate("./build/template-min.js", VARIABLES))
.pipe(removeMappedSource("notmapped.js"))
.pipe(rename(function (path) { path.basename = "jdenticon"; path.extname = ".min.js" }))
.pipe(sourcemaps.write(".", { includeContent: true }))
.pipe(gulp.dest("dist"))
.pipe(rename(function (path) { path.basename = "jdenticon-" + pack.version; path.extname = ".min.js" }))
.pipe(sourcemaps.write(".", { includeContent: true }))
.pipe(gulp.dest("obj/output"));
});
gulp.task("build-cjs", function () {
return gulp.src("./src/browser-cjs.js")
.pipe(sourcemaps.init())
.pipe(rollup({
output: { format: "cjs" },
plugins: [ stripBanner() ],
}))
.pipe(rename(function (path) { path.basename = "notmapped"; path.extname = ".js" }))
.pipe(buble())
.pipe(preMinify())
.pipe(removeJsDocImports())
// Replace variables
.pipe(replace(VARIABLES))
.pipe(wrapTemplate("./build/template-module.js", VARIABLES))
.pipe(removeMappedSource("notmapped.js"))
.pipe(rename(function (path) { path.basename = "jdenticon-module"; path.extname = ".js" }))
.pipe(replace(/[\r\n]*$/, "\n//# sourceMappingURL=jdenticon-module.js.map\n"))
.pipe(sourcemaps.write("./", { includeContent: true, addComment: false }))
.pipe(gulp.dest("dist"))
.pipe(rename(function (path) { path.basename = "jdenticon-module-" + pack.version; path.extname = ".js" }))
.pipe(gulp.dest("obj/output"))
});
gulp.task("build-esm", function () {
return gulp.src("./src/browser-esm.js")
.pipe(sourcemaps.init())
.pipe(rollup({
output: { format: "esm" },
plugins: [ stripBanner() ],
}))
.pipe(preMinify())
.pipe(removeJsDocImports())
// Replace variables
.pipe(replace(VARIABLES))
.pipe(wrapTemplate("./build/template-module.js", VARIABLES))
.pipe(rename(function (path) { path.basename = "jdenticon-module"; path.extname = ".mjs" }))
.pipe(replace(/[\r\n]*$/, "\n//# sourceMappingURL=jdenticon-module.mjs.map\n"))
.pipe(sourcemaps.write("./", { includeContent: true, addComment: false }))
.pipe(gulp.dest("dist"))
.pipe(rename(function (path) { path.basename = "jdenticon-module-" + pack.version; path.extname = ".mjs" }))
.pipe(gulp.dest("obj/output"))
});
gulp.task("build-node-cjs", function () {
return gulp.src("./src/node-cjs.js")
.pipe(sourcemaps.init())
.pipe(rollup({
external: [ "canvas-renderer" ],
plugins: [ stripBanner(), commonjs() ],
output: { format: "cjs" },
}))
.pipe(removeJsDocImports())
.pipe(replace(VARIABLES))
.pipe(wrapTemplate("./build/template-module.js", VARIABLES))
.pipe(rename(path => { path.basename = "jdenticon-node"; path.extname = ".js" }))
.pipe(replace(/[\r\n]*$/, "\n//# sourceMappingURL=jdenticon-node.js.map\n"))
.pipe(sourcemaps.write("./", { includeContent: true, addComment: false }))
.pipe(gulp.dest("./dist"))
.pipe(rename(function (path) { path.basename = "jdenticon-node-" + pack.version; path.extname = ".js" }))
.pipe(gulp.dest("./obj/output"));
});
gulp.task("build-node-esm", function () {
return gulp.src("./src/node-esm.js")
.pipe(sourcemaps.init())
.pipe(rollup({
external: [ "canvas-renderer" ],
plugins: [ stripBanner(), commonjs() ],
output: { format: "esm" },
}))
.pipe(removeJsDocImports())
.pipe(replace(VARIABLES))
.pipe(wrapTemplate("./build/template-module.js", VARIABLES))
.pipe(rename(path => { path.basename = "jdenticon-node"; path.extname = ".mjs" }))
.pipe(replace(/[\r\n]*$/, "\n//# sourceMappingURL=jdenticon-node.mjs.map\n"))
.pipe(sourcemaps.write("./", { includeContent: true, addComment: false }))
.pipe(gulp.dest("./dist"))
.pipe(rename(function (path) { path.basename = "jdenticon-node-" + pack.version; path.extname = ".mjs" }))
.pipe(gulp.dest("./obj/output"));
});
gulp.task("update-license-year", function () {
return gulp.src("./LICENSE")
.pipe(replace(/\(c\) 2014-\d+/, "(c) 2014-" + new Date().getFullYear()))
.pipe(gulp.dest("./"))
});
gulp.task("update-readme", function () {
return gulp.src("./README.md")
.pipe(replace([
[/@\d{1,2}\.\d{1,3}\.\d{1,3}/, "@" + pack.version],
[/(?<=integrity=\"([^-]+)-).*?(?=\")/, (match, algorithm) => {
const min = fs.readFileSync("./dist/jdenticon.min.js");
return crypto.createHash(algorithm).update(min).digest("base64");
}],
]))
.pipe(gulp.dest("./"))
});
gulp.task("install-jdenticon-test", function () {
const globs = pack.files
.map(file => {
const isDirectory =
!/\*/.test(file) &&
fs.existsSync(file) &&
fs.lstatSync(file).isDirectory();
return isDirectory ? path.join(file, "**") : file;
});
// Simulate an installed Jdenticon package. Cannot use actual npm command, since it won't install Jdenticon in a
// subfolder to the Jdenticon source package itself.
return gulp.src(["./package.json", ...globs], { base: "./" })
.pipe(gulp.dest("./test/node_modules/jdenticon"));
});
gulp.task("build", gulp.series("clean", gulp.parallel(
"build-umd", "build-umd-min",
"build-esm", "build-cjs",
"build-node-cjs", "build-node-esm",
), "install-jdenticon-test"));
gulp.task("clean-tests", function () {
return del(["./obj/test/unit/**"]);
});
gulp.task("build-unit-tests-js", function () {
return gulp.src("./test/unit/*.js", { base: "./" })
.pipe(sourcemaps.init())
.pipe(rollup({
external: [ "canvas-renderer", "fs", "tap" ],
plugins: [ commonjs() ],
output: { format: "cjs" },
}))
.pipe(sourcemaps.write("./"))
.pipe(gulp.dest("./obj"))
});
gulp.task("build-unit-tests", gulp.series("clean-tests", "build-unit-tests-js"));
gulp.task("prepare-release", function () {
return gulp.src(["./LICENSE", "./README.md"])
.pipe(replace(VARIABLES))
.pipe(rename(function (path) { path.extname = ".txt" }))
.pipe(gulp.dest("obj/output"));
});
gulp.task("prepare-nuget", function () {
return gulp.src(["./build/jdenticon.nuspec"])
.pipe(replace(VARIABLES))
.pipe(rename(function (path) { path.basename = "~" + path.basename }))
.pipe(gulp.dest("./"));
});
gulp.task("nuget", async function () {
var command = "\"./build/nuget/nuget.exe\" pack ~jdenticon.nuspec -OutputDirectory releases";
if (process.platform !== "win32") {
command = "mono " + command;
}
await promisify(exec)(command);
await del(["./~jdenticon.nuspec"]);
});
gulp.task("create-package", function () {
return gulp.src(["./obj/output/*"])
.pipe(zip("jdenticon-" + pack.version + ".zip"))
.pipe(gulp.dest("releases"));
});
gulp.task("release", gulp.series(
"build",
"update-license-year", "update-readme",
"prepare-release",
"create-package",
"prepare-nuget", "nuget",
));

View File

@@ -0,0 +1,4 @@
{
"main": "../dist/jdenticon-node",
"types": "../types/module.d.ts"
}

15357
jdenticon-js/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

126
jdenticon-js/package.json Normal file
View File

@@ -0,0 +1,126 @@
{
"name": "jdenticon",
"version": "3.3.0",
"description": "Javascript identicon generator",
"main": "dist/jdenticon-node.js",
"exports": {
".": {
"types": "./types/module.d.ts",
"node": {
"require": "./dist/jdenticon-node.js",
"import": "./dist/jdenticon-node.mjs"
},
"browser": {
"require": "./dist/jdenticon-module.js",
"import": "./dist/jdenticon-module.mjs"
},
"default": "./dist/jdenticon-node.js"
},
"./browser": {
"types": "./types/module.d.ts",
"import": "./dist/jdenticon-module.mjs",
"default": "./dist/jdenticon-module.js"
},
"./node": {
"types": "./types/module.d.ts",
"import": "./dist/jdenticon-node.mjs",
"default": "./dist/jdenticon-node.js"
},
"./standalone": {
"types": "./types/umd.d.ts",
"default": "./dist/jdenticon.min.js"
}
},
"browser": "dist/jdenticon-module",
"jsdelivr": "dist/jdenticon.min.js",
"unpkg": "dist/jdenticon.min.js",
"types": "types/module.d.ts",
"bin": {
"jdenticon": "bin/jdenticon.js"
},
"engines": {
"node": ">=6.4.0"
},
"dependencies": {
"canvas-renderer": "~2.2.0"
},
"devDependencies": {
"@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-node-resolve": "^13.3.0",
"@types/jquery": "^3.5.14",
"@types/node14": "npm:@types/node@14",
"@types/node16": "npm:@types/node@16",
"acorn": "^8.8.0",
"blink-diff": "^1.0.13",
"buble": "^0.20.0",
"del": "^6.1.1",
"eslint": "^8.21.0",
"express": "^4.18.1",
"gulp": "^4.0.2",
"gulp-buble": "^0.9.0",
"gulp-rename": "^2.0.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-terser": "^2.1.0",
"gulp-zip": "^5.1.0",
"module-alias": "^2.2.2",
"pngjs": "^6.0.0",
"rollup": "^2.77.2",
"rollup-plugin-strip-banner": "^2.0.0",
"rollup-plugin-terser": "^7.0.2",
"selenium-webdriver": "^4.20.0",
"source-map-loader": "^1.1.3",
"tap": "^16.3.0",
"typescript3": "npm:typescript@^3.2.4",
"typescript4": "npm:typescript@^4.7.4",
"webpack4": "npm:webpack@4",
"webpack5": "npm:webpack@5"
},
"tap": {
"check-coverage": false
},
"scripts": {
"build": "npm run lint && npm run tsc3 -- -p src/tsconfig.json && gulp build",
"release": "gulp release",
"lint": "eslint ./src/**/*.js",
"test:unit": "gulp build-unit-tests && tap obj/test/unit/*.js",
"test:types": "npm run test:types:browser && npm run test:types:node14 && npm run test:types:node16 && npm run test:types:umd",
"test:types:browser": "npm run tsc3 -- -p ./test/types/module-browser/tsconfig.json && npm run tsc4 -- -p ./test/types/module-browser/tsconfig.json --moduleResolution node16",
"test:types:node14": "npm run tsc3 -- -p ./test/types/module-node14/tsconfig.json",
"test:types:node16": "npm run tsc4 -- -p ./test/types/module-node16/tsconfig.json && npm run tsc4 -- -p ./test/types/module-node16/tsconfig.json --moduleResolution node16",
"test:types:umd": "npm run tsc3 -- -p ./test/types/umd/tsconfig.json && npm run tsc4 -- -p ./test/types/umd/tsconfig.json --moduleResolution node16",
"test:browser-win": "tap ./test/e2e/browser/test.js --test-arg=win --test-arg=chrome,firefox,edge,ie11,ie10,ie9 --timeout=300",
"test:browser-macos": "tap ./test/e2e/browser/test.js --test-arg=macos --test-arg=chrome,firefox,safari --timeout=300",
"test:node-cjs": "tap ./test/e2e/node/test.js",
"test:node-esm": "tap ./test/e2e/node/test.mjs",
"test:webpack4": "cd ./test/e2e/webpack && node runner.js webpack4 && tap ./app.bundle.js --no-coverage",
"test:webpack5": "cd ./test/e2e/webpack && node runner.js webpack5 && tap ./app.bundle.js --no-coverage",
"test:rollup": "cd ./test/e2e/rollup && rollup --config && tap ./app.bundle.js --no-coverage",
"tsc3": "node ./node_modules/typescript3/bin/tsc",
"tsc4": "node ./node_modules/typescript4/bin/tsc"
},
"files": [
"bin/",
"dist/",
"standalone/",
"browser/",
"node/",
"types/*.d.ts"
],
"repository": {
"type": "git",
"url": "https://github.com/dmester/jdenticon"
},
"bugs": {
"url": "https://github.com/dmester/jdenticon/issues"
},
"keywords": [
"javascript",
"identicon",
"jdenticon",
"avatar"
],
"author": "Daniel Mester Pirttijärvi",
"license": "MIT",
"homepage": "https://jdenticon.com/"
}

View File

@@ -0,0 +1,2 @@
export { configure } from "../common/configuration";

View File

@@ -0,0 +1,28 @@
import { iconGenerator } from "../renderer/iconGenerator";
import { isValidHash, computeHash } from "../common/hashUtils";
import { CanvasRenderer } from "../renderer/canvas/canvasRenderer";
import { IS_RENDERED_PROPERTY } from "../common/dom";
/**
* Draws an identicon to a context.
* @param {CanvasRenderingContext2D} ctx - Canvas context on which the icon will be drawn at location (0, 0).
* @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon.
* @param {number} size - Icon size in pixels.
* @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any
* global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be
* specified in place of a configuration object.
*/
export function drawIcon(ctx, hashOrValue, size, config) {
if (!ctx) {
throw new Error("No canvas specified.");
}
iconGenerator(new CanvasRenderer(ctx, size),
isValidHash(hashOrValue) || computeHash(hashOrValue),
config);
const canvas = ctx.canvas;
if (canvas) {
canvas[IS_RENDERED_PROPERTY] = true;
}
}

18
jdenticon-js/src/apis/jquery.js vendored Normal file
View File

@@ -0,0 +1,18 @@
import { update } from "./update";
/**
* Renders an identicon for all matching supported elements.
*
* @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon. If not
* specified the `data-jdenticon-hash` and `data-jdenticon-value` attributes of each element will be
* evaluated.
* @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any global
* configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be
* specified in place of a configuration object.
*/
export function jdenticonJqueryPlugin(hashOrValue, config) {
this["each"](function (index, el) {
update(el, hashOrValue, config);
});
return this;
}

View File

@@ -0,0 +1,24 @@
import canvasRenderer from "canvas-renderer";
import { iconGenerator } from "../renderer/iconGenerator";
import { isValidHash, computeHash } from "../common/hashUtils";
import { CanvasRenderer } from "../renderer/canvas/canvasRenderer";
/**
* Draws an identicon as PNG.
* @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon.
* @param {number} size - Icon size in pixels.
* @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any
* global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be
* specified in place of a configuration object.
* @returns {Buffer} PNG data
*/
export function toPng(hashOrValue, size, config) {
const canvas = canvasRenderer.createCanvas(size, size);
const ctx = canvas.getContext("2d");
iconGenerator(new CanvasRenderer(ctx, size),
isValidHash(hashOrValue) || computeHash(hashOrValue),
config);
return canvas.toPng({ "Software": "Jdenticon" });
}

View File

@@ -0,0 +1,21 @@
import { iconGenerator } from "../renderer/iconGenerator";
import { isValidHash, computeHash } from "../common/hashUtils";
import { SvgRenderer } from "../renderer/svg/svgRenderer";
import { SvgWriter } from "../renderer/svg/svgWriter";
/**
* Draws an identicon as an SVG string.
* @param {*} hashOrValue - A hexadecimal hash string or any value that will be hashed by Jdenticon.
* @param {number} size - Icon size in pixels.
* @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any
* global configuration in its entirety. For backward compatibility a padding value in the range [0.0, 0.5) can be
* specified in place of a configuration object.
* @returns {string} SVG string
*/
export function toSvg(hashOrValue, size, config) {
const writer = new SvgWriter(size);
iconGenerator(new SvgRenderer(writer),
isValidHash(hashOrValue) || computeHash(hashOrValue),
config);
return writer.toString();
}

View File

@@ -0,0 +1,149 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { iconGenerator } from "../renderer/iconGenerator";
import { isValidHash, computeHash } from "../common/hashUtils";
import { ATTRIBUTES, ICON_SELECTOR, IS_RENDERED_PROPERTY, documentQuerySelectorAll } from "../common/dom";
import { SvgRenderer } from "../renderer/svg/svgRenderer";
import { SvgElement } from "../renderer/svg/svgElement";
import { CanvasRenderer } from "../renderer/canvas/canvasRenderer";
import { ICON_TYPE_CANVAS, ICON_TYPE_SVG, getIdenticonType } from "../common/dom";
/**
* Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute.
*/
export function updateAll() {
if (documentQuerySelectorAll) {
update(ICON_SELECTOR);
}
}
/**
* Updates all canvas elements with the `data-jdenticon-hash` or `data-jdenticon-value` attribute that have not already
* been rendered.
*/
export function updateAllConditional() {
if (documentQuerySelectorAll) {
/** @type {NodeListOf<HTMLElement>} */
const elements = documentQuerySelectorAll(ICON_SELECTOR);
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
if (!el[IS_RENDERED_PROPERTY]) {
update(el);
}
}
}
}
/**
* Updates the identicon in the specified `<canvas>` or `<svg>` elements.
* @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type
* `<svg>` or `<canvas>`, or a CSS selector to such an element.
* @param {*=} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or
* `data-jdenticon-value` attribute will be evaluated.
* @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any
* global configuration in its entirety. For backward compability a padding value in the range [0.0, 0.5) can be
* specified in place of a configuration object.
*/
export function update(el, hashOrValue, config) {
renderDomElement(el, hashOrValue, config, function (el, iconType) {
if (iconType) {
return iconType == ICON_TYPE_SVG ?
new SvgRenderer(new SvgElement(el)) :
new CanvasRenderer(/** @type {HTMLCanvasElement} */(el).getContext("2d"));
}
});
}
/**
* Updates the identicon in the specified `<canvas>` elements.
* @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type
* `<canvas>`, or a CSS selector to such an element.
* @param {*=} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or
* `data-jdenticon-value` attribute will be evaluated.
* @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any
* global configuration in its entirety. For backward compability a padding value in the range [0.0, 0.5) can be
* specified in place of a configuration object.
*/
export function updateCanvas(el, hashOrValue, config) {
renderDomElement(el, hashOrValue, config, function (el, iconType) {
if (iconType == ICON_TYPE_CANVAS) {
return new CanvasRenderer(/** @type {HTMLCanvasElement} */(el).getContext("2d"));
}
});
}
/**
* Updates the identicon in the specified `<svg>` elements.
* @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type
* `<svg>`, or a CSS selector to such an element.
* @param {*=} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or
* `data-jdenticon-value` attribute will be evaluated.
* @param {Object|number=} config - Optional configuration. If specified, this configuration object overrides any
* global configuration in its entirety. For backward compability a padding value in the range [0.0, 0.5) can be
* specified in place of a configuration object.
*/
export function updateSvg(el, hashOrValue, config) {
renderDomElement(el, hashOrValue, config, function (el, iconType) {
if (iconType == ICON_TYPE_SVG) {
return new SvgRenderer(new SvgElement(el));
}
});
}
/**
* Updates the identicon in the specified canvas or svg elements.
* @param {(string|Element)} el - Specifies the container in which the icon is rendered as a DOM element of the type
* `<svg>` or `<canvas>`, or a CSS selector to such an element.
* @param {*} hashOrValue - Optional hash or value to be rendered. If not specified, the `data-jdenticon-hash` or
* `data-jdenticon-value` attribute will be evaluated.
* @param {Object|number|undefined} config
* @param {function(Element,number):import("../renderer/renderer").Renderer} rendererFactory - Factory function for creating an icon renderer.
*/
function renderDomElement(el, hashOrValue, config, rendererFactory) {
if (typeof el === "string") {
if (documentQuerySelectorAll) {
const elements = documentQuerySelectorAll(el);
for (let i = 0; i < elements.length; i++) {
renderDomElement(elements[i], hashOrValue, config, rendererFactory);
}
}
return;
}
// Hash selection. The result from getValidHash or computeHash is
// accepted as a valid hash.
const hash =
// 1. Explicit valid hash
isValidHash(hashOrValue) ||
// 2. Explicit value (`!= null` catches both null and undefined)
hashOrValue != null && computeHash(hashOrValue) ||
// 3. `data-jdenticon-hash` attribute
isValidHash(el.getAttribute(ATTRIBUTES.HASH)) ||
// 4. `data-jdenticon-value` attribute.
// We want to treat an empty attribute as an empty value.
// Some browsers return empty string even if the attribute
// is not specified, so use hasAttribute to determine if
// the attribute is specified.
el.hasAttribute(ATTRIBUTES.VALUE) && computeHash(el.getAttribute(ATTRIBUTES.VALUE));
if (!hash) {
// No hash specified. Don't render an icon.
return;
}
const renderer = rendererFactory(el, getIdenticonType(el));
if (renderer) {
// Draw icon
iconGenerator(renderer, hash, config);
el[IS_RENDERED_PROPERTY] = true;
}
}

View File

@@ -0,0 +1,39 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
// This file is compiled to dist/jdenticon-module.js
import { defineConfigProperty } from "./common/configuration";
import { configure } from "./apis/configure";
import { drawIcon } from "./apis/drawIcon";
import { toSvg } from "./apis/toSvg";
import { update, updateAll, updateCanvas, updateSvg } from "./apis/update";
const jdenticon = updateAll;
defineConfigProperty(jdenticon);
// Export public API
jdenticon["configure"] = configure;
jdenticon["drawIcon"] = drawIcon;
jdenticon["toSvg"] = toSvg;
jdenticon["update"] = update;
jdenticon["updateCanvas"] = updateCanvas;
jdenticon["updateSvg"] = updateSvg;
/**
* Specifies the version of the Jdenticon package in use.
* @type {string}
*/
jdenticon["version"] = "#version#";
/**
* Specifies which bundle of Jdenticon that is used.
* @type {string}
*/
jdenticon["bundle"] = "browser-cjs";
module.exports = jdenticon;

View File

@@ -0,0 +1,24 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
// This file is compiled to dist/jdenticon-module.mjs
export { configure } from "./apis/configure";
export { drawIcon } from "./apis/drawIcon";
export { toSvg } from "./apis/toSvg";
export { update, updateCanvas, updateSvg } from "./apis/update";
/**
* Specifies the version of the Jdenticon package in use.
* @type {string}
*/
export const version = "#version#";
/**
* Specifies which bundle of Jdenticon that is used.
* @type {string}
*/
export const bundle = "browser-esm";

View File

@@ -0,0 +1,71 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
// This file is compiled to dist/jdenticon.js and dist/jdenticon.min.js
import { CONFIG_PROPERTIES, defineConfigProperty } from "./common/configuration";
import { observer } from "./common/observer";
import { configure } from "./apis/configure";
import { drawIcon } from "./apis/drawIcon";
import { toSvg } from "./apis/toSvg";
import { update, updateAll, updateAllConditional } from "./apis/update";
import { jdenticonJqueryPlugin } from "./apis/jquery";
import { GLOBAL } from "./common/global";
import { whenDocumentIsReady } from "./common/dom";
const jdenticon = updateAll;
defineConfigProperty(jdenticon);
// Export public API
jdenticon["configure"] = configure;
jdenticon["drawIcon"] = drawIcon;
jdenticon["toSvg"] = toSvg;
jdenticon["update"] = update;
jdenticon["updateCanvas"] = update;
jdenticon["updateSvg"] = update;
/**
* Specifies the version of the Jdenticon package in use.
* @type {string}
*/
jdenticon["version"] = "#version#";
/**
* Specifies which bundle of Jdenticon that is used.
* @type {string}
*/
jdenticon["bundle"] = "browser-umd";
// Basic jQuery plugin
const jQuery = GLOBAL["jQuery"];
if (jQuery) {
jQuery["fn"]["jdenticon"] = jdenticonJqueryPlugin;
}
/**
* This function is called once upon page load.
*/
function jdenticonStartup() {
const replaceMode = (
jdenticon[CONFIG_PROPERTIES.MODULE] ||
GLOBAL[CONFIG_PROPERTIES.GLOBAL] ||
{ }
)["replaceMode"];
if (replaceMode != "never") {
updateAllConditional();
if (replaceMode == "observe") {
observer(update);
}
}
}
// Schedule to render all identicons on the page once it has been loaded.
whenDocumentIsReady(jdenticonStartup);
module.exports = jdenticon;

View File

@@ -0,0 +1,152 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { parseColor } from "../renderer/color";
import { GLOBAL } from "./global";
/**
* @typedef {Object} ParsedConfiguration
* @property {number} colorSaturation
* @property {number} grayscaleSaturation
* @property {string} backColor
* @property {number} iconPadding
* @property {function(number):number} hue
* @property {function(number):number} colorLightness
* @property {function(number):number} grayscaleLightness
*/
export const CONFIG_PROPERTIES = {
GLOBAL: "jdenticon_config",
MODULE: "config",
};
var rootConfigurationHolder = {};
/**
* Defines the deprecated `config` property on the root Jdenticon object. When the property is set a warning is
* printed in the console. To minimize bundle size, this is only used in Node bundles.
* @param {!Object} rootObject
*/
export function defineConfigPropertyWithWarn(rootObject) {
Object.defineProperty(rootObject, CONFIG_PROPERTIES.MODULE, {
configurable: true,
get: () => rootConfigurationHolder[CONFIG_PROPERTIES.MODULE],
set: newConfiguration => {
rootConfigurationHolder[CONFIG_PROPERTIES.MODULE] = newConfiguration;
console.warn("jdenticon.config is deprecated. Use jdenticon.configure() instead.");
},
});
}
/**
* Defines the deprecated `config` property on the root Jdenticon object without printing a warning in the console
* when it is being used.
* @param {!Object} rootObject
*/
export function defineConfigProperty(rootObject) {
rootConfigurationHolder = rootObject;
}
/**
* Sets a new icon style configuration. The new configuration is not merged with the previous one. *
* @param {Object} newConfiguration - New configuration object.
*/
export function configure(newConfiguration) {
if (arguments.length) {
rootConfigurationHolder[CONFIG_PROPERTIES.MODULE] = newConfiguration;
}
return rootConfigurationHolder[CONFIG_PROPERTIES.MODULE];
}
/**
* Gets the normalized current Jdenticon color configuration. Missing fields have default values.
* @param {Object|number|undefined} paddingOrLocalConfig - Configuration passed to the called API method. A
* local configuration overrides the global configuration in it entirety. This parameter can for backward
* compatibility also contain a padding value. A padding value only overrides the global padding, not the
* entire global configuration.
* @param {number} defaultPadding - Padding used if no padding is specified in neither the configuration nor
* explicitly to the API method.
* @returns {ParsedConfiguration}
*/
export function getConfiguration(paddingOrLocalConfig, defaultPadding) {
const configObject =
typeof paddingOrLocalConfig == "object" && paddingOrLocalConfig ||
rootConfigurationHolder[CONFIG_PROPERTIES.MODULE] ||
GLOBAL[CONFIG_PROPERTIES.GLOBAL] ||
{ },
lightnessConfig = configObject["lightness"] || { },
// In versions < 2.1.0 there was no grayscale saturation -
// saturation was the color saturation.
saturation = configObject["saturation"] || { },
colorSaturation = "color" in saturation ? saturation["color"] : saturation,
grayscaleSaturation = saturation["grayscale"],
backColor = configObject["backColor"],
padding = configObject["padding"];
/**
* Creates a lightness range.
*/
function lightness(configName, defaultRange) {
let range = lightnessConfig[configName];
// Check if the lightness range is an array-like object. This way we ensure the
// array contain two values at the same time.
if (!(range && range.length > 1)) {
range = defaultRange;
}
/**
* Gets a lightness relative the specified value in the specified lightness range.
*/
return function (value) {
value = range[0] + value * (range[1] - range[0]);
return value < 0 ? 0 : value > 1 ? 1 : value;
};
}
/**
* Gets a hue allowed by the configured hue restriction,
* provided the originally computed hue.
*/
function hueFunction(originalHue) {
const hueConfig = configObject["hues"];
let hue;
// Check if 'hues' is an array-like object. This way we also ensure that
// the array is not empty, which would mean no hue restriction.
if (hueConfig && hueConfig.length > 0) {
// originalHue is in the range [0, 1]
// Multiply with 0.999 to change the range to [0, 1) and then truncate the index.
hue = hueConfig[0 | (0.999 * originalHue * hueConfig.length)];
}
return typeof hue == "number" ?
// A hue was specified. We need to convert the hue from
// degrees on any turn - e.g. 746° is a perfectly valid hue -
// to turns in the range [0, 1).
((((hue / 360) % 1) + 1) % 1) :
// No hue configured => use original hue
originalHue;
}
return {
hue: hueFunction,
colorSaturation: typeof colorSaturation == "number" ? colorSaturation : 0.5,
grayscaleSaturation: typeof grayscaleSaturation == "number" ? grayscaleSaturation : 0,
colorLightness: lightness("color", [0.4, 0.8]),
grayscaleLightness: lightness("grayscale", [0.3, 0.9]),
backColor: parseColor(backColor),
iconPadding:
typeof paddingOrLocalConfig == "number" ? paddingOrLocalConfig :
typeof padding == "number" ? padding :
defaultPadding
}
}

View File

@@ -0,0 +1,56 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
export const ICON_TYPE_SVG = 1;
export const ICON_TYPE_CANVAS = 2;
export const ATTRIBUTES = {
HASH: "data-jdenticon-hash",
VALUE: "data-jdenticon-value"
};
export const IS_RENDERED_PROPERTY = "jdenticonRendered";
export const ICON_SELECTOR = "[" + ATTRIBUTES.HASH +"],[" + ATTRIBUTES.VALUE +"]";
export const documentQuerySelectorAll = /** @type {!Function} */ (
typeof document !== "undefined" && document.querySelectorAll.bind(document));
export function getIdenticonType(el) {
if (el) {
const tagName = el["tagName"];
if (/^svg$/i.test(tagName)) {
return ICON_TYPE_SVG;
}
if (/^canvas$/i.test(tagName) && "getContext" in el) {
return ICON_TYPE_CANVAS;
}
}
}
export function whenDocumentIsReady(/** @type {Function} */ callback) {
function loadedHandler() {
document.removeEventListener("DOMContentLoaded", loadedHandler);
window.removeEventListener("load", loadedHandler);
setTimeout(callback, 0); // Give scripts a chance to run
}
if (typeof document !== "undefined" &&
typeof window !== "undefined" &&
typeof setTimeout !== "undefined"
) {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadedHandler);
window.addEventListener("load", loadedHandler);
} else {
// Document already loaded. The load events above likely won't be raised
setTimeout(callback, 0);
}
}
}

View File

@@ -0,0 +1,14 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
// In the future we can replace `GLOBAL` with `globalThis`, but for now use the old school global detection for
// backward compatibility.
export const GLOBAL =
typeof window !== "undefined" ? window :
typeof self !== "undefined" ? self :
typeof global !== "undefined" ? global :
{};

View File

@@ -0,0 +1,10 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
/* global umdGlobal */
// In the future we can replace `GLOBAL` with `globalThis`, but for now use the old school global detection for
// backward compatibility.
export const GLOBAL = umdGlobal;

View File

@@ -0,0 +1,23 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { sha1 } from "./sha1";
/**
* Inputs a value that might be a valid hash string for Jdenticon and returns it
* if it is determined valid, otherwise a falsy value is returned.
*/
export function isValidHash(hashCandidate) {
return /^[0-9a-f]{11,}$/i.test(hashCandidate) && hashCandidate;
}
/**
* Computes a hash for the specified value. Currently SHA1 is used. This function
* always returns a valid hash.
*/
export function computeHash(value) {
return sha1(value == null ? "" : "" + value);
}

View File

@@ -0,0 +1,47 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { ATTRIBUTES, ICON_SELECTOR, getIdenticonType } from "./dom";
export function observer(updateCallback) {
if (typeof MutationObserver != "undefined") {
const mutationObserver = new MutationObserver(function onmutation(mutations) {
for (let mutationIndex = 0; mutationIndex < mutations.length; mutationIndex++) {
const mutation = mutations[mutationIndex];
const addedNodes = mutation.addedNodes;
for (let addedNodeIndex = 0; addedNodes && addedNodeIndex < addedNodes.length; addedNodeIndex++) {
const addedNode = addedNodes[addedNodeIndex];
// Skip other types of nodes than element nodes, since they might not support
// the querySelectorAll method => runtime error.
if (addedNode.nodeType == Node.ELEMENT_NODE) {
if (getIdenticonType(addedNode)) {
updateCallback(addedNode);
}
else {
const icons = /** @type {Element} */(addedNode).querySelectorAll(ICON_SELECTOR);
for (let iconIndex = 0; iconIndex < icons.length; iconIndex++) {
updateCallback(icons[iconIndex]);
}
}
}
}
if (mutation.type == "attributes" && getIdenticonType(mutation.target)) {
updateCallback(mutation.target);
}
}
});
mutationObserver.observe(document.body, {
"childList": true,
"attributes": true,
"attributeFilter": [ATTRIBUTES.VALUE, ATTRIBUTES.HASH, "width", "height"],
"subtree": true,
});
}
}

View File

@@ -0,0 +1,14 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
/**
* Parses a substring of the hash as a number.
* @param {number} startPosition
* @param {number=} octets
*/
export function parseHex(hash, startPosition, octets) {
return parseInt(hash.substr(startPosition, octets), 16);
}

View File

@@ -0,0 +1,132 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
/**
* Computes a SHA1 hash for any value and returns it as a hexadecimal string.
*
* This function is optimized for minimal code size and rather short messages.
*
* @param {string} message
*/
export function sha1(message) {
const HASH_SIZE_HALF_BYTES = 40;
const BLOCK_SIZE_WORDS = 16;
// Variables
// `var` is used to be able to minimize the number of `var` keywords.
var i = 0,
f = 0,
// Use `encodeURI` to UTF8 encode the message without any additional libraries
// We could use `unescape` + `encodeURI` to minimize the code, but that would be slightly risky
// since `unescape` is deprecated.
urlEncodedMessage = encodeURI(message) + "%80", // trailing '1' bit padding
// This can be changed to a preallocated Uint32Array array for greater performance and larger code size
data = [],
dataSize,
hashBuffer = [],
a = 0x67452301,
b = 0xefcdab89,
c = ~a,
d = ~b,
e = 0xc3d2e1f0,
hash = [a, b, c, d, e],
blockStartIndex = 0,
hexHash = "";
/**
* Rotates the value a specified number of bits to the left.
* @param {number} value Value to rotate
* @param {number} shift Bit count to shift.
*/
function rotl(value, shift) {
return (value << shift) | (value >>> (32 - shift));
}
// Message data
for ( ; i < urlEncodedMessage.length; f++) {
data[f >> 2] = data[f >> 2] |
(
(
urlEncodedMessage[i] == "%"
// Percent encoded byte
? parseInt(urlEncodedMessage.substring(i + 1, i += 3), 16)
// Unencoded byte
: urlEncodedMessage.charCodeAt(i++)
)
// Read bytes in reverse order (big endian words)
<< ((3 - (f & 3)) * 8)
);
}
// f is now the length of the utf8 encoded message
// 7 = 8 bytes (64 bit) for message size, -1 to round down
// >> 6 = integer division with block size
dataSize = (((f + 7) >> 6) + 1) * BLOCK_SIZE_WORDS;
// Message size in bits.
// SHA1 uses a 64 bit integer to represent the size, but since we only support short messages only the least
// significant 32 bits are set. -8 is for the '1' bit padding byte.
data[dataSize - 1] = f * 8 - 8;
// Compute hash
for ( ; blockStartIndex < dataSize; blockStartIndex += BLOCK_SIZE_WORDS) {
for (i = 0; i < 80; i++) {
f = rotl(a, 5) + e + (
// Ch
i < 20 ? ((b & c) ^ ((~b) & d)) + 0x5a827999 :
// Parity
i < 40 ? (b ^ c ^ d) + 0x6ed9eba1 :
// Maj
i < 60 ? ((b & c) ^ (b & d) ^ (c & d)) + 0x8f1bbcdc :
// Parity
(b ^ c ^ d) + 0xca62c1d6
) + (
hashBuffer[i] = i < BLOCK_SIZE_WORDS
// Bitwise OR is used to coerse `undefined` to 0
? (data[blockStartIndex + i] | 0)
: rotl(hashBuffer[i - 3] ^ hashBuffer[i - 8] ^ hashBuffer[i - 14] ^ hashBuffer[i - 16], 1)
);
e = d;
d = c;
c = rotl(b, 30);
b = a;
a = f;
}
hash[0] = a = ((hash[0] + a) | 0);
hash[1] = b = ((hash[1] + b) | 0);
hash[2] = c = ((hash[2] + c) | 0);
hash[3] = d = ((hash[3] + d) | 0);
hash[4] = e = ((hash[4] + e) | 0);
}
// Format hex hash
for (i = 0; i < HASH_SIZE_HALF_BYTES; i++) {
hexHash += (
(
// Get word (2^3 half-bytes per word)
hash[i >> 3] >>>
// Append half-bytes in reverse order
((7 - (i & 7)) * 4)
)
// Clamp to half-byte
& 0xf
).toString(16);
}
return hexHash;
}

View File

@@ -0,0 +1,72 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
// This file is compiled to dist/jdenticon-node.js
if (typeof process === "undefined" &&
typeof window !== "undefined" &&
typeof document !== "undefined"
) {
console.warn(
"Jdenticon: 'dist/jdenticon-node.js' is only intended for Node.js environments and will increase your " +
"bundle size when included in browser bundles. If you want to run Jdenticon in the browser, please add a " +
"reference to 'dist/jdenticon.js' or 'dist/jdenticon.min.js' instead.");
}
import { defineConfigPropertyWithWarn } from "./common/configuration";
import { configure } from "./apis/configure";
import { drawIcon } from "./apis/drawIcon";
import { toPng } from "./apis/toPng";
import { toSvg } from "./apis/toSvg";
/**
* @throws {Error}
*/
function jdenticon() {
throw new Error("jdenticon() is not supported on Node.js.");
}
defineConfigPropertyWithWarn(jdenticon);
jdenticon.configure = configure;
jdenticon.drawIcon = drawIcon;
jdenticon.toPng = toPng;
jdenticon.toSvg = toSvg;
/**
* Specifies the version of the Jdenticon package in use.
* @type {string}
*/
jdenticon.version = "#version#";
/**
* Specifies which bundle of Jdenticon that is used.
* @type {string}
*/
jdenticon.bundle = "node-cjs";
/**
* @throws {Error}
*/
jdenticon.update = function update() {
throw new Error("jdenticon.update() is not supported on Node.js.");
};
/**
* @throws {Error}
*/
jdenticon.updateCanvas = function updateCanvas() {
throw new Error("jdenticon.updateCanvas() is not supported on Node.js.");
};
/**
* @throws {Error}
*/
jdenticon.updateSvg = function updateSvg() {
throw new Error("jdenticon.updateSvg() is not supported on Node.js.");
};
module.exports = jdenticon;

View File

@@ -0,0 +1,55 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
// This file is compiled to dist/jdenticon-node.mjs
if (typeof process === "undefined" &&
typeof window !== "undefined" &&
typeof document !== "undefined"
) {
console.warn(
"Jdenticon: 'dist/jdenticon-node.mjs' is only intended for Node.js environments and will increase your " +
"bundle size when included in browser bundles. If you want to run Jdenticon in the browser, please add a " +
"reference to 'dist/jdenticon.js' or 'dist/jdenticon.min.js' instead.");
}
export { configure } from "./apis/configure";
export { drawIcon } from "./apis/drawIcon";
export { toPng } from "./apis/toPng";
export { toSvg } from "./apis/toSvg";
/**
* Specifies the version of the Jdenticon package in use.
* @type {string}
*/
export const version = "#version#";
/**
* Specifies which bundle of Jdenticon that is used.
* @type {string}
*/
export const bundle = "node-esm";
/**
* @throws {Error}
*/
export function update() {
throw new Error("jdenticon.update() is not supported on Node.js.");
}
/**
* @throws {Error}
*/
export function updateCanvas() {
throw new Error("jdenticon.updateCanvas() is not supported on Node.js.");
}
/**
* @throws {Error}
*/
export function updateSvg() {
throw new Error("jdenticon.updateSvg() is not supported on Node.js.");
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,108 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { toCss3Color } from "../color";
/**
* @typedef {import("../renderer").Renderer} Renderer
* @typedef {import('../point').Point} Point
*/
/**
* Renderer redirecting drawing commands to a canvas context.
* @implements {Renderer}
*/
export class CanvasRenderer {
/**
* @param {number=} iconSize
*/
constructor(ctx, iconSize) {
const canvas = ctx.canvas;
const width = canvas.width;
const height = canvas.height;
ctx.save();
if (!iconSize) {
iconSize = Math.min(width, height);
ctx.translate(
((width - iconSize) / 2) | 0,
((height - iconSize) / 2) | 0);
}
/**
* @private
*/
this._ctx = ctx;
this.iconSize = iconSize;
ctx.clearRect(0, 0, iconSize, iconSize);
}
/**
* Fills the background with the specified color.
* @param {string} fillColor Fill color on the format #rrggbb[aa].
*/
setBackground(fillColor) {
const ctx = this._ctx;
const iconSize = this.iconSize;
ctx.fillStyle = toCss3Color(fillColor);
ctx.fillRect(0, 0, iconSize, iconSize);
}
/**
* Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape.
* @param {string} fillColor Fill color on format #rrggbb[aa].
*/
beginShape(fillColor) {
const ctx = this._ctx;
ctx.fillStyle = toCss3Color(fillColor);
ctx.beginPath();
}
/**
* Marks the end of the currently drawn shape. This causes the queued paths to be rendered on the canvas.
*/
endShape() {
this._ctx.fill();
}
/**
* Adds a polygon to the rendering queue.
* @param points An array of Point objects.
*/
addPolygon(points) {
const ctx = this._ctx;
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length; i++) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.closePath();
}
/**
* Adds a circle to the rendering queue.
* @param {Point} point The upper left corner of the circle bounding box.
* @param {number} diameter The diameter of the circle.
* @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).
*/
addCircle(point, diameter, counterClockwise) {
const ctx = this._ctx,
radius = diameter / 2;
ctx.moveTo(point.x + radius, point.y + radius);
ctx.arc(point.x + radius, point.y + radius, radius, 0, Math.PI * 2, counterClockwise);
ctx.closePath();
}
/**
* Called when the icon has been completely drawn.
*/
finish() {
this._ctx.restore();
}
}

View File

@@ -0,0 +1,123 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { parseHex } from "../common/parseHex";
function decToHex(v) {
v |= 0; // Ensure integer value
return v < 0 ? "00" :
v < 16 ? "0" + v.toString(16) :
v < 256 ? v.toString(16) :
"ff";
}
function hueToRgb(m1, m2, h) {
h = h < 0 ? h + 6 : h > 6 ? h - 6 : h;
return decToHex(255 * (
h < 1 ? m1 + (m2 - m1) * h :
h < 3 ? m2 :
h < 4 ? m1 + (m2 - m1) * (4 - h) :
m1));
}
/**
* @param {number} r Red channel [0, 255]
* @param {number} g Green channel [0, 255]
* @param {number} b Blue channel [0, 255]
*/
export function rgb(r, g, b) {
return "#" + decToHex(r) + decToHex(g) + decToHex(b);
}
/**
* @param {string} color Color value to parse. Currently hexadecimal strings on the format #rgb[a] and #rrggbb[aa] are supported.
* @returns {string}
*/
export function parseColor(color) {
if (/^#[0-9a-f]{3,8}$/i.test(color)) {
let result;
const colorLength = color.length;
if (colorLength < 6) {
const r = color[1],
g = color[2],
b = color[3],
a = color[4] || "";
result = "#" + r + r + g + g + b + b + a + a;
}
if (colorLength == 7 || colorLength > 8) {
result = color;
}
return result;
}
}
/**
* Converts a hexadecimal color to a CSS3 compatible color.
* @param {string} hexColor Color on the format "#RRGGBB" or "#RRGGBBAA"
* @returns {string}
*/
export function toCss3Color(hexColor) {
const a = parseHex(hexColor, 7, 2);
let result;
if (isNaN(a)) {
result = hexColor;
} else {
const r = parseHex(hexColor, 1, 2),
g = parseHex(hexColor, 3, 2),
b = parseHex(hexColor, 5, 2);
result = "rgba(" + r + "," + g + "," + b + "," + (a / 255).toFixed(2) + ")";
}
return result;
}
/**
* Converts an HSL color to a hexadecimal RGB color.
* @param {number} hue Hue in range [0, 1]
* @param {number} saturation Saturation in range [0, 1]
* @param {number} lightness Lightness in range [0, 1]
* @returns {string}
*/
export function hsl(hue, saturation, lightness) {
// Based on http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color
let result;
if (saturation == 0) {
const partialHex = decToHex(lightness * 255);
result = partialHex + partialHex + partialHex;
}
else {
const m2 = lightness <= 0.5 ? lightness * (saturation + 1) : lightness + saturation - lightness * saturation,
m1 = lightness * 2 - m2;
result =
hueToRgb(m1, m2, hue * 6 + 2) +
hueToRgb(m1, m2, hue * 6) +
hueToRgb(m1, m2, hue * 6 - 2);
}
return "#" + result;
}
/**
* Converts an HSL color to a hexadecimal RGB color. This function will correct the lightness for the "dark" hues
* @param {number} hue Hue in range [0, 1]
* @param {number} saturation Saturation in range [0, 1]
* @param {number} lightness Lightness in range [0, 1]
* @returns {string}
*/
export function correctedHsl(hue, saturation, lightness) {
// The corrector specifies the perceived middle lightness for each hue
const correctors = [ 0.55, 0.5, 0.5, 0.46, 0.6, 0.55, 0.55 ],
corrector = correctors[(hue * 6 + 0.5) | 0];
// Adjust the input lightness relative to the corrector
lightness = lightness < 0.5 ? lightness * corrector * 2 : corrector + (lightness - 0.5) * (1 - corrector) * 2;
return hsl(hue, saturation, lightness);
}

View File

@@ -0,0 +1,28 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { correctedHsl } from "./color";
/**
* Gets a set of identicon color candidates for a specified hue and config.
* @param {number} hue
* @param {import("../common/configuration").ParsedConfiguration} config
*/
export function colorTheme(hue, config) {
hue = config.hue(hue);
return [
// Dark gray
correctedHsl(hue, config.grayscaleSaturation, config.grayscaleLightness(0)),
// Mid color
correctedHsl(hue, config.colorSaturation, config.colorLightness(0.5)),
// Light gray
correctedHsl(hue, config.grayscaleSaturation, config.grayscaleLightness(1)),
// Light color
correctedHsl(hue, config.colorSaturation, config.colorLightness(1)),
// Dark color
correctedHsl(hue, config.colorSaturation, config.colorLightness(0))
];
}

View File

@@ -0,0 +1,116 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { NO_TRANSFORM } from "./transform";
/**
* @typedef {import("./renderer").Renderer} Renderer
* @typedef {import("./transform").Transform} Transform
*/
/**
* Provides helper functions for rendering common basic shapes.
*/
export class Graphics {
/**
* @param {Renderer} renderer
*/
constructor(renderer) {
/**
* @type {Renderer}
* @private
*/
this._renderer = renderer;
/**
* @type {Transform}
*/
this.currentTransform = NO_TRANSFORM;
}
/**
* Adds a polygon to the underlying renderer.
* @param {Array<number>} points The points of the polygon clockwise on the format [ x0, y0, x1, y1, ..., xn, yn ]
* @param {boolean=} invert Specifies if the polygon will be inverted.
*/
addPolygon(points, invert) {
const di = invert ? -2 : 2,
transformedPoints = [];
for (let i = invert ? points.length - 2 : 0; i < points.length && i >= 0; i += di) {
transformedPoints.push(this.currentTransform.transformIconPoint(points[i], points[i + 1]));
}
this._renderer.addPolygon(transformedPoints);
}
/**
* Adds a polygon to the underlying renderer.
* Source: http://stackoverflow.com/a/2173084
* @param {number} x The x-coordinate of the upper left corner of the rectangle holding the entire ellipse.
* @param {number} y The y-coordinate of the upper left corner of the rectangle holding the entire ellipse.
* @param {number} size The size of the ellipse.
* @param {boolean=} invert Specifies if the ellipse will be inverted.
*/
addCircle(x, y, size, invert) {
const p = this.currentTransform.transformIconPoint(x, y, size, size);
this._renderer.addCircle(p, size, invert);
}
/**
* Adds a rectangle to the underlying renderer.
* @param {number} x The x-coordinate of the upper left corner of the rectangle.
* @param {number} y The y-coordinate of the upper left corner of the rectangle.
* @param {number} w The width of the rectangle.
* @param {number} h The height of the rectangle.
* @param {boolean=} invert Specifies if the rectangle will be inverted.
*/
addRectangle(x, y, w, h, invert) {
this.addPolygon([
x, y,
x + w, y,
x + w, y + h,
x, y + h
], invert);
}
/**
* Adds a right triangle to the underlying renderer.
* @param {number} x The x-coordinate of the upper left corner of the rectangle holding the triangle.
* @param {number} y The y-coordinate of the upper left corner of the rectangle holding the triangle.
* @param {number} w The width of the triangle.
* @param {number} h The height of the triangle.
* @param {number} r The rotation of the triangle (clockwise). 0 = right corner of the triangle in the lower left corner of the bounding rectangle.
* @param {boolean=} invert Specifies if the triangle will be inverted.
*/
addTriangle(x, y, w, h, r, invert) {
const points = [
x + w, y,
x + w, y + h,
x, y + h,
x, y
];
points.splice(((r || 0) % 4) * 2, 2);
this.addPolygon(points, invert);
}
/**
* Adds a rhombus to the underlying renderer.
* @param {number} x The x-coordinate of the upper left corner of the rectangle holding the rhombus.
* @param {number} y The y-coordinate of the upper left corner of the rectangle holding the rhombus.
* @param {number} w The width of the rhombus.
* @param {number} h The height of the rhombus.
* @param {boolean=} invert Specifies if the rhombus will be inverted.
*/
addRhombus(x, y, w, h, invert) {
this.addPolygon([
x + w / 2, y,
x + w, y + h / 2,
x + w / 2, y + h,
x, y + h / 2
], invert);
}
}

View File

@@ -0,0 +1,95 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { Transform } from "./transform";
import { Graphics } from "./graphics";
import { centerShape, outerShape } from "./shapes";
import { colorTheme } from "./colorTheme";
import { parseHex } from "../common/parseHex";
import { getConfiguration } from "../common/configuration";
/**
* Draws an identicon to a specified renderer.
* @param {import('./renderer').Renderer} renderer
* @param {string} hash
* @param {Object|number=} config
*/
export function iconGenerator(renderer, hash, config) {
const parsedConfig = getConfiguration(config, 0.08);
// Set background color
if (parsedConfig.backColor) {
renderer.setBackground(parsedConfig.backColor);
}
// Calculate padding and round to nearest integer
let size = renderer.iconSize;
const padding = (0.5 + size * parsedConfig.iconPadding) | 0;
size -= padding * 2;
const graphics = new Graphics(renderer);
// Calculate cell size and ensure it is an integer
const cell = 0 | (size / 4);
// Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon
const x = 0 | (padding + size / 2 - cell * 2);
const y = 0 | (padding + size / 2 - cell * 2);
function renderShape(colorIndex, shapes, index, rotationIndex, positions) {
const shapeIndex = parseHex(hash, index, 1);
let r = rotationIndex ? parseHex(hash, rotationIndex, 1) : 0;
renderer.beginShape(availableColors[selectedColorIndexes[colorIndex]]);
for (let i = 0; i < positions.length; i++) {
graphics.currentTransform = new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4);
shapes(shapeIndex, graphics, cell, i);
}
renderer.endShape();
}
// AVAILABLE COLORS
const hue = parseHex(hash, -7) / 0xfffffff,
// Available colors for this icon
availableColors = colorTheme(hue, parsedConfig),
// The index of the selected colors
selectedColorIndexes = [];
let index;
function isDuplicate(values) {
if (values.indexOf(index) >= 0) {
for (let i = 0; i < values.length; i++) {
if (selectedColorIndexes.indexOf(values[i]) >= 0) {
return true;
}
}
}
}
for (let i = 0; i < 3; i++) {
index = parseHex(hash, 8 + i, 1) % availableColors.length;
if (isDuplicate([0, 4]) || // Disallow dark gray and dark color combo
isDuplicate([2, 3])) { // Disallow light gray and light color combo
index = 1;
}
selectedColorIndexes.push(index);
}
// ACTUAL RENDERING
// Sides
renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]);
// Corners
renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]);
// Center
renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]);
renderer.finish();
}

View File

@@ -0,0 +1,19 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
/**
* Represents a point.
*/
export class Point {
/**
* @param {number} x
* @param {number} y
*/
constructor(x, y) {
this.x = x;
this.y = y;
}
}

View File

@@ -0,0 +1,18 @@
/**
* @typedef {import('./point').Point} Point
*/
/**
* @typedef {Object} Renderer
* @property {number} iconSize
* @property {function(string):void} setBackground Fills the background with the specified color.
* @property {function(string):void} beginShape Marks the beginning of a new shape of the specified color. Should be
* ended with a call to endShape.
* @property {function():void} endShape Marks the end of the currently drawn shape. This causes the queued paths to
* be rendered on the canvas.
* @property {function(Point[]):void} addPolygon Adds a polygon to the rendering queue.
* @property {function(Point,number,boolean):void} addCircle Adds a circle to the rendering queue.
* @property {function():void} finish Called when the icon has been completely drawn.
*/
export default class {}

View File

@@ -0,0 +1,154 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
/**
* @param {number} index
* @param {Graphics} g
* @param {number} cell
* @param {number} positionIndex
* @typedef {import('./graphics').Graphics} Graphics
*/
export function centerShape(index, g, cell, positionIndex) {
index = index % 14;
let k, m, w, h, inner, outer;
!index ? (
k = cell * 0.42,
g.addPolygon([
0, 0,
cell, 0,
cell, cell - k * 2,
cell - k, cell,
0, cell
])) :
index == 1 ? (
w = 0 | (cell * 0.5),
h = 0 | (cell * 0.8),
g.addTriangle(cell - w, 0, w, h, 2)) :
index == 2 ? (
w = 0 | (cell / 3),
g.addRectangle(w, w, cell - w, cell - w)) :
index == 3 ? (
inner = cell * 0.1,
// Use fixed outer border widths in small icons to ensure the border is drawn
outer =
cell < 6 ? 1 :
cell < 8 ? 2 :
(0 | (cell * 0.25)),
inner =
inner > 1 ? (0 | inner) : // large icon => truncate decimals
inner > 0.5 ? 1 : // medium size icon => fixed width
inner, // small icon => anti-aliased border
g.addRectangle(outer, outer, cell - inner - outer, cell - inner - outer)) :
index == 4 ? (
m = 0 | (cell * 0.15),
w = 0 | (cell * 0.5),
g.addCircle(cell - w - m, cell - w - m, w)) :
index == 5 ? (
inner = cell * 0.1,
outer = inner * 4,
// Align edge to nearest pixel in large icons
outer > 3 && (outer = 0 | outer),
g.addRectangle(0, 0, cell, cell),
g.addPolygon([
outer, outer,
cell - inner, outer,
outer + (cell - outer - inner) / 2, cell - inner
], true)) :
index == 6 ?
g.addPolygon([
0, 0,
cell, 0,
cell, cell * 0.7,
cell * 0.4, cell * 0.4,
cell * 0.7, cell,
0, cell
]) :
index == 7 ?
g.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 3) :
index == 8 ? (
g.addRectangle(0, 0, cell, cell / 2),
g.addRectangle(0, cell / 2, cell / 2, cell / 2),
g.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 1)) :
index == 9 ? (
inner = cell * 0.14,
// Use fixed outer border widths in small icons to ensure the border is drawn
outer =
cell < 4 ? 1 :
cell < 6 ? 2 :
(0 | (cell * 0.35)),
inner =
cell < 8 ? inner : // small icon => anti-aliased border
(0 | inner), // large icon => truncate decimals
g.addRectangle(0, 0, cell, cell),
g.addRectangle(outer, outer, cell - outer - inner, cell - outer - inner, true)) :
index == 10 ? (
inner = cell * 0.12,
outer = inner * 3,
g.addRectangle(0, 0, cell, cell),
g.addCircle(outer, outer, cell - inner - outer, true)) :
index == 11 ?
g.addTriangle(cell / 2, cell / 2, cell / 2, cell / 2, 3) :
index == 12 ? (
m = cell * 0.25,
g.addRectangle(0, 0, cell, cell),
g.addRhombus(m, m, cell - m, cell - m, true)) :
// 13
(
!positionIndex && (
m = cell * 0.4, w = cell * 1.2,
g.addCircle(m, m, w)
)
);
}
/**
* @param {number} index
* @param {Graphics} g
* @param {number} cell
*/
export function outerShape(index, g, cell) {
index = index % 4;
let m;
!index ?
g.addTriangle(0, 0, cell, cell, 0) :
index == 1 ?
g.addTriangle(0, cell / 2, cell, cell / 2, 0) :
index == 2 ?
g.addRhombus(0, 0, cell, cell) :
// 3
(
m = cell / 6,
g.addCircle(m, m, cell - 2 * m)
);
}

View File

@@ -0,0 +1,11 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
export const SVG_CONSTANTS = {
XMLNS: "http://www.w3.org/2000/svg",
WIDTH: "width",
HEIGHT: "height",
}

View File

@@ -0,0 +1,88 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { SVG_CONSTANTS } from "./constants";
/**
* Creates a new element and adds it to the specified parent.
* @param {Element} parentNode
* @param {string} name
* @param {...(string|number)} keyValuePairs
*/
function SvgElement_append(parentNode, name, ...keyValuePairs) {
const el = document.createElementNS(SVG_CONSTANTS.XMLNS, name);
for (let i = 0; i + 1 < keyValuePairs.length; i += 2) {
el.setAttribute(
/** @type {string} */(keyValuePairs[i]),
/** @type {string} */(keyValuePairs[i + 1]),
);
}
parentNode.appendChild(el);
}
/**
* Renderer producing SVG output.
*/
export class SvgElement {
/**
* @param {Element} element - Target element
*/
constructor(element) {
// Don't use the clientWidth and clientHeight properties on SVG elements
// since Firefox won't serve a proper value of these properties on SVG
// elements (https://bugzilla.mozilla.org/show_bug.cgi?id=874811)
// Instead use 100px as a hardcoded size (the svg viewBox will rescale
// the icon to the correct dimensions)
const iconSize = this.iconSize = Math.min(
(Number(element.getAttribute(SVG_CONSTANTS.WIDTH)) || 100),
(Number(element.getAttribute(SVG_CONSTANTS.HEIGHT)) || 100)
);
/**
* @type {Element}
* @private
*/
this._el = element;
// Clear current SVG child elements
while (element.firstChild) {
element.removeChild(element.firstChild);
}
// Set viewBox attribute to ensure the svg scales nicely.
element.setAttribute("viewBox", "0 0 " + iconSize + " " + iconSize);
element.setAttribute("preserveAspectRatio", "xMidYMid meet");
}
/**
* Fills the background with the specified color.
* @param {string} fillColor Fill color on the format #rrggbb.
* @param {number} opacity Opacity in the range [0.0, 1.0].
*/
setBackground(fillColor, opacity) {
if (opacity) {
SvgElement_append(this._el, "rect",
SVG_CONSTANTS.WIDTH, "100%",
SVG_CONSTANTS.HEIGHT, "100%",
"fill", fillColor,
"opacity", opacity);
}
}
/**
* Appends a path to the SVG element.
* @param {string} color Fill color on format #xxxxxx.
* @param {string} dataString The SVG path data string.
*/
appendPath(color, dataString) {
SvgElement_append(this._el, "path",
"fill", color,
"d", dataString);
}
}

View File

@@ -0,0 +1,58 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
/**
* Prepares a measure to be used as a measure in an SVG path, by
* rounding the measure to a single decimal. This reduces the file
* size of the generated SVG with more than 50% in some cases.
*/
function svgValue(value) {
return ((value * 10 + 0.5) | 0) / 10;
}
/**
* Represents an SVG path element.
*/
export class SvgPath {
constructor() {
/**
* This property holds the data string (path.d) of the SVG path.
* @type {string}
*/
this.dataString = "";
}
/**
* Adds a polygon with the current fill color to the SVG path.
* @param points An array of Point objects.
*/
addPolygon(points) {
let dataString = "";
for (let i = 0; i < points.length; i++) {
dataString += (i ? "L" : "M") + svgValue(points[i].x) + " " + svgValue(points[i].y);
}
this.dataString += dataString + "Z";
}
/**
* Adds a circle with the current fill color to the SVG path.
* @param {import('../point').Point} point The upper left corner of the circle bounding box.
* @param {number} diameter The diameter of the circle.
* @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).
*/
addCircle(point, diameter, counterClockwise) {
const sweepFlag = counterClockwise ? 0 : 1,
svgRadius = svgValue(diameter / 2),
svgDiameter = svgValue(diameter),
svgArc = "a" + svgRadius + "," + svgRadius + " 0 1," + sweepFlag + " ";
this.dataString +=
"M" + svgValue(point.x) + " " + svgValue(point.y + diameter / 2) +
svgArc + svgDiameter + ",0" +
svgArc + (-svgDiameter) + ",0";
}
}

View File

@@ -0,0 +1,104 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { SvgPath } from "./svgPath";
import { parseHex } from "../../common/parseHex";
/**
* @typedef {import("../point").Point} Point
* @typedef {import("../renderer").Renderer} Renderer
* @typedef {import("./svgElement").SvgElement} SvgElement
* @typedef {import("./svgWriter").SvgWriter} SvgWriter
*/
/**
* Renderer producing SVG output.
* @implements {Renderer}
*/
export class SvgRenderer {
/**
* @param {SvgElement|SvgWriter} target
*/
constructor(target) {
/**
* @type {SvgPath}
* @private
*/
this._path;
/**
* @type {Object.<string,SvgPath>}
* @private
*/
this._pathsByColor = { };
/**
* @type {SvgElement|SvgWriter}
* @private
*/
this._target = target;
/**
* @type {number}
*/
this.iconSize = target.iconSize;
}
/**
* Fills the background with the specified color.
* @param {string} fillColor Fill color on the format #rrggbb[aa].
*/
setBackground(fillColor) {
const match = /^(#......)(..)?/.exec(fillColor),
opacity = match[2] ? parseHex(match[2], 0) / 255 : 1;
this._target.setBackground(match[1], opacity);
}
/**
* Marks the beginning of a new shape of the specified color. Should be ended with a call to endShape.
* @param {string} color Fill color on format #xxxxxx.
*/
beginShape(color) {
this._path = this._pathsByColor[color] || (this._pathsByColor[color] = new SvgPath());
}
/**
* Marks the end of the currently drawn shape.
*/
endShape() { }
/**
* Adds a polygon with the current fill color to the SVG.
* @param points An array of Point objects.
*/
addPolygon(points) {
this._path.addPolygon(points);
}
/**
* Adds a circle with the current fill color to the SVG.
* @param {Point} point The upper left corner of the circle bounding box.
* @param {number} diameter The diameter of the circle.
* @param {boolean} counterClockwise True if the circle is drawn counter-clockwise (will result in a hole if rendered on a clockwise path).
*/
addCircle(point, diameter, counterClockwise) {
this._path.addCircle(point, diameter, counterClockwise);
}
/**
* Called when the icon has been completely drawn.
*/
finish() {
const pathsByColor = this._pathsByColor;
for (let color in pathsByColor) {
// hasOwnProperty cannot be shadowed in pathsByColor
// eslint-disable-next-line no-prototype-builtins
if (pathsByColor.hasOwnProperty(color)) {
this._target.appendPath(color, pathsByColor[color].dataString);
}
}
}
}

View File

@@ -0,0 +1,59 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { SVG_CONSTANTS } from "./constants";
/**
* Renderer producing SVG output.
*/
export class SvgWriter {
/**
* @param {number} iconSize - Icon width and height in pixels.
*/
constructor(iconSize) {
/**
* @type {number}
*/
this.iconSize = iconSize;
/**
* @type {string}
* @private
*/
this._s =
'<svg xmlns="' + SVG_CONSTANTS.XMLNS + '" width="' +
iconSize + '" height="' + iconSize + '" viewBox="0 0 ' +
iconSize + ' ' + iconSize + '">';
}
/**
* Fills the background with the specified color.
* @param {string} fillColor Fill color on the format #rrggbb.
* @param {number} opacity Opacity in the range [0.0, 1.0].
*/
setBackground(fillColor, opacity) {
if (opacity) {
this._s += '<rect width="100%" height="100%" fill="' +
fillColor + '" opacity="' + opacity.toFixed(2) + '"/>';
}
}
/**
* Writes a path to the SVG string.
* @param {string} color Fill color on format #rrggbb.
* @param {string} dataString The SVG path data string.
*/
appendPath(color, dataString) {
this._s += '<path fill="' + color + '" d="' + dataString + '"/>';
}
/**
* Gets the rendered image as an SVG string.
*/
toString() {
return this._s + "</svg>";
}
}

View File

@@ -0,0 +1,45 @@
/**
* Jdenticon
* https://github.com/dmester/jdenticon
* Copyright © Daniel Mester Pirttijärvi
*/
import { Point } from "./point";
/**
* Translates and rotates a point before being passed on to the canvas context. This was previously done by the canvas context itself,
* but this caused a rendering issue in Chrome on sizes > 256 where the rotation transformation of inverted paths was not done properly.
*/
export class Transform {
/**
* @param {number} x The x-coordinate of the upper left corner of the transformed rectangle.
* @param {number} y The y-coordinate of the upper left corner of the transformed rectangle.
* @param {number} size The size of the transformed rectangle.
* @param {number} rotation Rotation specified as 0 = 0 rad, 1 = 0.5π rad, 2 = π rad, 3 = 1.5π rad
*/
constructor(x, y, size, rotation) {
this._x = x;
this._y = y;
this._size = size;
this._rotation = rotation;
}
/**
* Transforms the specified point based on the translation and rotation specification for this Transform.
* @param {number} x x-coordinate
* @param {number} y y-coordinate
* @param {number=} w The width of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle.
* @param {number=} h The height of the transformed rectangle. If greater than 0, this will ensure the returned point is of the upper left corner of the transformed rectangle.
*/
transformIconPoint(x, y, w, h) {
const right = this._x + this._size,
bottom = this._y + this._size,
rotation = this._rotation;
return rotation === 1 ? new Point(right - y - (h || 0), this._y + x) :
rotation === 2 ? new Point(right - x - (w || 0), bottom - y - (h || 0)) :
rotation === 3 ? new Point(this._x + y, bottom - x - (w || 0)) :
new Point(this._x + x, this._y + y);
}
}
export const NO_TRANSFORM = new Transform(0, 0, 0, 0);

View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"checkJs": true,
"noEmit": true,
"types": ["node14"],
"lib": ["es6", "dom"]
},
"include": ["*.js"]
}

View File

@@ -0,0 +1,4 @@
{
"main": "../dist/jdenticon.min.js",
"types": "../types/umd.d.ts"
}

2
jdenticon-js/test/e2e/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.bundle.js
*.bundle.js.map

View File

@@ -0,0 +1,40 @@
const tap = require("tap");
const canvasRenderer = require("canvas-renderer");
export function testBrowser(jdenticon, bundle) {
tap.test(bundle, bundleTest => {
bundleTest.test("jdenticon.bundle", t => {
t.equal(jdenticon.bundle, bundle);
t.end();
});
bundleTest.test("jdenticon.version", t => {
t.match(jdenticon.version, /^\d+(\.\d+)*$/);
t.end();
});
bundleTest.test("jdenticon.configure", t => {
t.doesNotThrow(() => jdenticon.configure({ backColor: "#fff" }));
t.end();
});
bundleTest.test("jdenticon.drawIcon", t => {
t.doesNotThrow(() => jdenticon.drawIcon(canvasRenderer.createCanvas(100, 100).getContext("2d"), "Icon1", 100));
t.end();
});
bundleTest.test("jdenticon.toSvg", t => {
t.match(jdenticon.toSvg("Icon1", 100), /^<svg/);
t.end();
});
bundleTest.test("jdenticon.update*", t => {
t.type(jdenticon.update, Function);
t.type(jdenticon.updateCanvas, Function);
t.type(jdenticon.updateSvg, Function);
t.end();
});
bundleTest.end();
});
}

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>Jdenticon</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<h1>"Icon0" - AMD - Should be equal</h1>
<figure>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAWlBMVEUAAP9aOuYxMYKdne+CXN5AQGLHx+lDQ2TIyOlDQ13NzedHR1ZHR1jW1ufY2OdLS1Dg4OVKSk/g4OWjc9Gjc9FLS02ldNDj4+RLS0yldNDj4+VMTEymddHl5eU2yqFLAAAAG3RSTlMRHy8vPVtbXFxra4iIiIjDw8TE0tPw8PDx8fEmYw1hAAABrUlEQVR42u2ZYWvCQBBE6542UVuT2Kaxnvf//2aR45hrl2mgxyKFfR+HjM9EggP35DiOY0x4fn0OP0PZveyEhLTOFWO6M367ToZ4ZxAe6jrnsKTMckC4n2Nm3tNQ1ylhSYUllFDmWJiFhrrOGBMYSzhEMNBQ1wkh1YQcSqwRErK6pk81fQ67WNORkNU1p1RzyuEx1hxJSOrmd/KA3wScEziXcIpgoqGuM8I1Fa54Ty6xcBEa6jrlcC0X1W/8pXzcnoe6zgnnfLOhDmXKj0VISOtc0596dY10x05oqOuO8xAwD2zrmAeEprqeB4Smup4HhJa6ngeMlrr+KyK01PWfKqOhTuaBYrNtqKuvQhxvn9uG+q8PFY7b7WP7x7qeB9TBLOt1PQ+Yg1nW63oeMAezrNf1POAObuF1Mg+Yg1t4ncMdsFDaHbBYOMwtm3ctwatvYYHDygKHwRKABQ6DIQELHO1DQlvgMBgSsMBhMSRggaN9SBBLdtgMCVjg+K9DAlgOCWA5JIDlkAA2Q4LTPiQ4JkMCrNcdx482/Gij5Whj8qMNP9rwow0/2nAcx7HjC8pg1fBM8ggRAAAAAElFTkSuQmCC" width="100" height="100">
<figcaption><strong>IMG</strong> static image</figcaption>
</figure>
<figure>
<canvas id="AMDIcon0" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> update(value) AMD</figcaption>
</figure>
<script>
var jdenticon_config = {
backColor: "#00f1",
replaceMode: "observe"
};
</script>
<script src="https://cdn.jsdelivr.net/requirejs/2.1.22/require.min.js"></script>
<script>
requirejs(["/node_modules/jdenticon/dist/jdenticon.min.js"], function (jd) {
jd.update("#AMDIcon0", "Icon0");
});
</script>
<script src="common.js"></script>
</body>
</html>

View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<title>Jdenticon</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<h1>"Icon2" - Should be centered vertically and horizontally</h1>
<figure>
<canvas data-jdenticon-value="Icon2" width="60" height="100"></canvas>
<figcaption><strong>Canvas</strong> data-jdenticon-value</figcaption>
</figure>
<figure>
<canvas id="canvas-Icon2-update-vert" width="60" height="100"></canvas>
<figcaption><strong>Canvas</strong> update(value)</figcaption>
</figure>
<figure>
<svg data-jdenticon-value="Icon2" width="60" height="100"></svg>
<figcaption><strong>SVG</strong> data-jdenticon-value</figcaption>
</figure>
<figure>
<svg id="svg-Icon2-update-vert" width="60" height="100"></svg>
<figcaption><strong>SVG</strong> update(value)</figcaption>
</figure>
<figure>
<canvas data-jdenticon-value="Icon2" width="100" height="60"></canvas>
<figcaption><strong>Canvas</strong> data-jdenticon-value</figcaption>
</figure>
<figure>
<canvas id="canvas-Icon2-update-hori" width="100" height="60"></canvas>
<figcaption><strong>Canvas</strong> update(value)</figcaption>
</figure>
<figure>
<svg data-jdenticon-value="Icon2" width="100" height="60"></svg>
<figcaption><strong>SVG</strong> data-jdenticon-value</figcaption>
</figure>
<figure>
<svg id="svg-Icon2-update-hori" width="100" height="60"></svg>
<figcaption><strong>SVG</strong> update(value)</figcaption>
</figure>
<script>
var jdenticon_config = {
backColor: "#00f1",
};
</script>
<script src="/node_modules/jdenticon/dist/jdenticon.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/require1k@1.0.1/require1k.min.js"></script>
<script>
jdenticon.update("#canvas-Icon2-update-vert,#svg-Icon2-update-vert", "Icon2");
jdenticon.update("#canvas-Icon2-update-hori,#svg-Icon2-update-hori", "Icon2");
</script>
<script src="common.js"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
addEventListener("message", function (ev) {
var data = JSON.parse(ev.data);
if ("scrollHeight" in data) {
var iframe = document.getElementsByName(data.name);
if (iframe && iframe.length) {
iframe[0].style.height = data.scrollHeight + "px";
}
}
});
function postHeight(timeout) {
setTimeout(function () {
// IE9 does not support passing objects through postMessage
window.parent.postMessage(JSON.stringify({
scrollHeight: document.body.scrollHeight,
name: window.name
}), "*");
}, timeout);
}
if (window.parent) {
postHeight(0);
postHeight(1000);
addEventListener("resize", postHeight);
}

View File

@@ -0,0 +1,66 @@
<html>
<head>
<title>Jdenticon browser test</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<script src="common.js"></script>
<div class="test-metadata">
<div class="jdenticon-info">Unknown Jdenticon version.</div>
<div class="browser-info">Unknown browser</div>
</div>
<!-- Tests are run in iframes to allow isolated testing of loaders and configuration -->
<iframe name="amd" src="amd.html"></iframe>
<iframe name="umd-in-head" src="umd-in-head.html"></iframe>
<iframe name="normal" src="normal.html"></iframe>
<iframe name="padding-0" src="padding.html?padding=0"></iframe>
<iframe name="padding-30" src="padding.html?padding=30"></iframe>
<iframe name="center" src="center.html"></iframe>
<script src="/node_modules/jdenticon/dist/jdenticon.min.js"></script>
<script>
var BROWSER_REGEX = ["Firefox/", "SamsungBrowser/", "Opera/", "OPR/", "MSIE ", "Trident/", "Edge?/", "Chrome/", "Safari/"];
function htmlEncode(text) {
var div = document.createElement("div");
div.innerText = text;
return div.innerHTML;
}
function detectBrowser() {
var html = navigator.userAgent;
var hasMatch = false;
for (var i = 0; !hasMatch && i < BROWSER_REGEX.length; i++) {
var tempHtml = html.replace(new RegExp("(\\b" + BROWSER_REGEX[i] + "\\S+)|<|>|&", "g"), function (match) {
if (match === "<") return "&lt;";
if (match === ">") return "&gt;";
if (match === "&") return "&amp;";
hasMatch = true;
return "<strong>" + htmlEncode(match) + "</strong>";
})
if (hasMatch) {
html = tempHtml;
}
}
if (!hasMatch) {
html = htmlEncode(html);
}
var browserInfo = document.querySelector(".browser-info");
browserInfo.innerHTML = html;
}
var versionInfo = document.querySelector(".jdenticon-info");
versionInfo.innerText = "Jdenticon " + jdenticon.version;
detectBrowser();
</script>
</body>
</html>

View File

@@ -0,0 +1,224 @@
<!DOCTYPE html>
<html>
<head>
<title>Jdenticon</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<h1>"Icon0" - Should be equal</h1>
<figure>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAWlBMVEUAAP9aOuYxMYKdne+CXN5AQGLHx+lDQ2TIyOlDQ13NzedHR1ZHR1jW1ufY2OdLS1Dg4OVKSk/g4OWjc9Gjc9FLS02ldNDj4+RLS0yldNDj4+VMTEymddHl5eU2yqFLAAAAG3RSTlMRHy8vPVtbXFxra4iIiIjDw8TE0tPw8PDx8fEmYw1hAAABrUlEQVR42u2ZYWvCQBBE6542UVuT2Kaxnvf//2aR45hrl2mgxyKFfR+HjM9EggP35DiOY0x4fn0OP0PZveyEhLTOFWO6M367ToZ4ZxAe6jrnsKTMckC4n2Nm3tNQ1ylhSYUllFDmWJiFhrrOGBMYSzhEMNBQ1wkh1YQcSqwRErK6pk81fQ67WNORkNU1p1RzyuEx1hxJSOrmd/KA3wScEziXcIpgoqGuM8I1Fa54Ty6xcBEa6jrlcC0X1W/8pXzcnoe6zgnnfLOhDmXKj0VISOtc0596dY10x05oqOuO8xAwD2zrmAeEprqeB4Smup4HhJa6ngeMlrr+KyK01PWfKqOhTuaBYrNtqKuvQhxvn9uG+q8PFY7b7WP7x7qeB9TBLOt1PQ+Yg1nW63oeMAezrNf1POAObuF1Mg+Yg1t4ncMdsFDaHbBYOMwtm3ctwatvYYHDygKHwRKABQ6DIQELHO1DQlvgMBgSsMBhMSRggaN9SBBLdtgMCVjg+K9DAlgOCWA5JIDlkAA2Q4LTPiQ4JkMCrNcdx482/Gij5Whj8qMNP9rwow0/2nAcx7HjC8pg1fBM8ggRAAAAAElFTkSuQmCC" width="100" height="100">
<figcaption><strong>IMG</strong> static image</figcaption>
</figure>
<figure>
<canvas data-jdenticon-value="Icon0" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> data-jdenticon-value</figcaption>
</figure>
<h1>"Icon0" - Should be equal to above, but black and no background</h1>
<figure>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAMFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABaPxwLAAAAD3RSTlMAECAwT1Bgf4C/wM/Q7/B+lp4GAAABKUlEQVR4Ae3ZwUrDUBSEYXtsYhLNPe//tiIlNPCDm5lQCvMtD8pP6GbgfkREXKzuX/eyHqHW/rOW7wjz3g/77DpC7X3Yy3SEtZ9WzxGqz8pyhKnPJssRlj5bxOMLvuTa3wS2ftpMR6jRh1GmI83Hn43ZdqTaHh9b+vE/NS1TmY8Rrx4SROKQIFKHBJE6JIjkIUGkDgkieUgQyUMCbp+8iUOCje/Bijgk2OjeWVGHBBqs6EOCDVb0IcEGK/qQYIMVfUiwwYo+JNhgRceGqYLG5ZXbT8NgxF6xNlDRG1wCrLChDwlW2NCHBCtsyEMCFTbkIYEKG/KQQIUNeUigwsZ7DgnyDwnyDwnyDwnyDwnyDwnyDwny/3tEnjbytJGnjTxt5GkjTxvXioj4BeoMdgqOkiMwAAAAAElFTkSuQmCC" width="100" height="100">
<figcaption><strong>IMG</strong> static image</figcaption>
</figure>
<figure>
<canvas id="explicit-config-update" data-jdenticon-value="Icon0" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> update()</figcaption>
</figure>
<figure>
<canvas id="explicit-config-drawIcon" data-jdenticon-value="Icon0" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> drawIcon()</figcaption>
</figure>
<figure>
<canvas id="explicit-config-jquery" data-jdenticon-value="Icon0" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> jQuery(value)</figcaption>
</figure>
<figure>
<span id="explicit-config-toSvg"></span>
<figcaption><strong>SVG</strong> toSvg(value)</figcaption>
</figure>
<h1>"Icon04" - Should be equal but different to the icon above</h1>
<figure>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAARVBMVEUAAP9tgO1thu1xiO50i+4xMYJAQGJDQ2RDQ11HR1ZHR1hLS1Cpx+RKSk+oxuSox+SqyeOqyuNLS01LS0xMTExZl8esy+PUIpVsAAAAFHRSTlMRKiotLi9bXGuIiMPDxMXN4uXw8Y2KdJAAAAGXSURBVHgB7JHbioNAEAVPLh11zewmY6///6lLWCTi1MjQkDfrsanyIKODAAcH5+/zx/PTY36cFAZzkOb4CuckgWa3r5tJeGzIQSo0G/3FaOIj5fsbpdZn/yf34iPn9Y1Ss+wL2VQ/ck4boI3+ZpT4yDlvgGa+xiQ+ck4bpHW+ppOER8633OeSu6TB1wyS8Mj5lsuzkJ6X9j/hHFZQan0TzmEFpeRvksRHznmFJJt8YTLxkfPaCkn9tHyuFx85r6+QZMlfJBMfOd9bQcm6oTMJjg05ruxLgZy0+EZ7fv25Kk4g/21Gcf7as2NUhYIwisErePvf7gPbbwrDKKKcdBaHILeYwP+FEvKgS5wLedAlzIE86BLmYh5ECXM458Hhq/r7gfMzPkVN4lx8VKPEuZgHUeL8/f/kk9/EPEgS52IeRIlzMQ+ixLmYB1HiXMyDKHEu0iW/9TJO0j98D4ki6SGRJT0kuqSHRJEsJBYSCwmR65AQuQ4JkeuQEGH+JHt+ZaeNnTZ22vjbaWOnjZ02dtrYaQNeNh9j/APLmwnC0xscngAAAABJRU5ErkJggg==" width="100" height="100">
<figcaption><strong>IMG</strong> static image</figcaption>
</figure>
<figure>
<canvas data-jdenticon-value="Icon04" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> data-jdenticon-value</figcaption>
</figure>
<figure>
<canvas data-jdenticon-hash="f97deee25dde01afb80ec1c7e4fae746492ddacb" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> data-jdenticon-hash</figcaption>
</figure>
<figure>
<canvas id="canvas-Jdenticon-update-hash" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> update(hash)</figcaption>
</figure>
<figure>
<canvas id="canvas-Jdenticon-update-value" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> update(value)</figcaption>
</figure>
<figure>
<canvas id="canvas-Jdenticon-jquery-hash" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> jQuery(hash)</figcaption>
</figure>
<figure>
<canvas id="canvas-Jdenticon-jquery-value" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> jQuery(value)</figcaption>
</figure>
<figure>
<canvas id="canvas-Jdenticon-drawIcon-hash" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> drawIcon(hash)</figcaption>
</figure>
<figure>
<canvas id="canvas-Jdenticon-drawIcon-value" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> drawIcon(value)</figcaption>
</figure>
<figure>
<span id="placeholder-canvas-Jdenticon-dynamic"></span>
<figcaption><strong>Canvas</strong> dynamic</figcaption>
</figure>
<figure>
<canvas id="canvas-Jdenticon-resize" data-jdenticon-value="Icon04" width="10" height="10"></canvas>
<figcaption><strong>Canvas</strong> resize</figcaption>
</figure>
<figure>
<svg data-jdenticon-value="Icon04" width="100" height="100"></svg>
<figcaption><strong>SVG</strong> data-jdenticon-value</figcaption>
</figure>
<figure>
<svg data-jdenticon-hash="f97deee25dde01afb80ec1c7e4fae746492ddacb" width="100" height="100"></svg>
<figcaption><strong>SVG</strong> data-jdenticon-hash</figcaption>
</figure>
<figure>
<svg id="svg-Jdenticon-update-hash" width="100" height="100"></svg>
<figcaption><strong>SVG</strong> update(hash)</figcaption>
</figure>
<figure>
<svg id="svg-Jdenticon-update-value" width="100" height="100"></svg>
<figcaption><strong>SVG</strong> update(value)</figcaption>
</figure>
<figure>
<svg id="svg-Jdenticon-jquery-hash" width="100" height="100"></svg>
<figcaption><strong>SVG</strong> jQuery(hash)</figcaption>
</figure>
<figure>
<svg id="svg-Jdenticon-jquery-value" width="100" height="100"></svg>
<figcaption><strong>SVG</strong> jQuery(value)</figcaption>
</figure>
<figure>
<span id="svg-Jdenticon-toSvg-hash"></span>
<figcaption><strong>SVG</strong> toSvg(hash)</figcaption>
</figure>
<figure>
<span id="svg-Jdenticon-toSvg-value"></span>
<figcaption><strong>SVG</strong> toSvg(value)</figcaption>
</figure>
<figure>
<span id="placeholder-svg-Jdenticon-dynamic"></span>
<figcaption><strong>SVG</strong> dynamic</figcaption>
</figure>
<figure>
<svg id="svg-Jdenticon-resize" data-jdenticon-value="Icon04" width="10" height="10"></svg>
<figcaption><strong>SVG</strong> resize</figcaption>
</figure>
<script>
var jdenticon_config = {
backColor: "#00f1",
replaceMode: "observe"
};
</script>
<script src="https://cdn.jsdelivr.net/jquery/3.2.1/jquery.slim.min.js"></script>
<script src="/node_modules/jdenticon/dist/jdenticon.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/require1k@1.0.1/require1k.min.js"></script>
<script>
// Explicit config
setTimeout(function () {
var explicitConfig = {
lightness: {
color: [0.00, 0.00],
grayscale: [0.00, 0.00]
},
saturation: {
color: 0.00,
grayscale: 0.00
},
padding: 0.08
};
jdenticon.update("#explicit-config-update", null, explicitConfig);
var ctx = document.getElementById("explicit-config-drawIcon").getContext("2d");
jdenticon.drawIcon(ctx, "Icon0", 100, explicitConfig);
document.getElementById("explicit-config-toSvg").innerHTML =
jdenticon.toSvg("Icon0", 100, explicitConfig);
$("#explicit-config-jquery").jdenticon("Icon0", explicitConfig);
});
// update()
jdenticon.update("#canvas-Jdenticon-update-value,#svg-Jdenticon-update-value", "Icon04");
jdenticon.update("#canvas-Jdenticon-update-hash,#svg-Jdenticon-update-hash", "f97deee25dde01afb80ec1c7e4fae746492ddacb");
// drawIcon()
var ctx = document.getElementById("canvas-Jdenticon-drawIcon-value").getContext("2d");
jdenticon.drawIcon(ctx, "Icon04", 100, 0.08);
var ctx = document.getElementById("canvas-Jdenticon-drawIcon-hash").getContext("2d");
jdenticon.drawIcon(ctx, "f97deee25dde01afb80ec1c7e4fae746492ddacb", 100, 0.08);
// toSvg()
document.getElementById("svg-Jdenticon-toSvg-value").innerHTML =
jdenticon.toSvg("Icon04", 100);
document.getElementById("svg-Jdenticon-toSvg-hash").innerHTML =
jdenticon.toSvg("f97deee25dde01afb80ec1c7e4fae746492ddacb", 100);
// jQuery
$("#canvas-Jdenticon-jquery-value,#svg-Jdenticon-jquery-value").jdenticon("Icon04");
$("#canvas-Jdenticon-jquery-hash,#svg-Jdenticon-jquery-hash").jdenticon("f97deee25dde01afb80ec1c7e4fae746492ddacb");
// Dynamic icons
setTimeout(function () {
// Canvas
document.getElementById("placeholder-canvas-Jdenticon-dynamic").innerHTML =
'<canvas data-jdenticon-value="Icon04" width="100" height="100"> Error </canvas>';
document.getElementById("canvas-Jdenticon-resize").setAttribute("width", "100");
document.getElementById("canvas-Jdenticon-resize").setAttribute("height", "100");
// Svg
document.getElementById("placeholder-svg-Jdenticon-dynamic").innerHTML =
'<svg data-jdenticon-value="Icon04" width="100" height="100"> Error </svg>';
document.getElementById("svg-Jdenticon-resize").setAttribute("width", "100");
document.getElementById("svg-Jdenticon-resize").setAttribute("height", "100");
}, 1000);
</script>
<script src="common.js"></script>
</body>
</html>

View File

@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html>
<head>
<title>Jdenticon</title>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<h1>"Icon04" - Should be equal - <span id="padding-percent"></span>% padding in style</h1>
<figure class="padding-0-only">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAUVBMVEUAAP8fH6JtgPNrgu1vhe5yiO81NXE4OGU/P1RCQlJBQVJCQk1DQ05ERExDQ0xERElEREqoxuSoxuSox+SqyeOqyeJFRUZFRUZGRkZZl8esy+Mq/AT0AAAAGHRSTlMSISorLi8/TnqIiaantbbExMTFzeLm8PFg+1g3AAABrUlEQVR4AazS2W6DMBQG4XEW0yQsTZsEyvs/aJEqy0AzF5b8337SHLGw2vHrSJ156vCYHwfKZymBucIVSWWocEVSGYTi5fZ5u0RwsJTAPwrd9Lcu4GApgR01rynt1eBgKYENhSWVYwEHT2UQ6qb1OjDwVAahOG0XwcBTCYS47lofgIGnBNKVdtdqARQ8dZ/f7w4w7FoDgIKnTs+38DyVP4mnFhIo/yaeWkig9O+SVCIB6DepHhwsJZSBMK5SY8DBU5kEaHJsbHDw1JoECOnF9AEHTe1IAOK1HdprBAdLCWUomaSEatyQVCKB8nmK8/eZGitI/RTvtz07RmEYBqAYev9bt1uXPgIaTDDfYweJYkME36wmmeSXalUCFFItS4ByqhUJUE61IgHKqfb/Ovnb9wClj2mTAKUsaBKgFDhNApRSrUmAOvBPjtzJkdflVGsSoJBqUQIUUi1KgEKqRQlQSLUoIcqnSxYSk7xK0p+w4y5IStwFSYi7IAlx1ySLu8Xd4u6CuHs+Le6CJMRdkIS4SxKiFhLvlWyW3Sy7WXaz7GbZzbKbZTfLbpa9YJb9AArjwlhWWmFfAAAAAElFTkSuQmCC" width="100" height="100">
<figcaption><strong>IMG</strong> static image</figcaption>
</figure>
<figure class="padding-30-only">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAJ1BMVEUAAP8wMIU4OGc4OGU+PlibteekwuOlwuRFRUZFRUZGRkZZl8esy+M8HoHgAAAACnRSTlMSME1Oa2ump/DxPobHuQAAANNJREFUeAHt1sEKgzAQhGFjklqN7/+8tSV1PCx42D0E+b+LCwO7IAxkwvMBAAAA9XWX+tV973vSPCdNSkNu9D1lOxRNSgNu9D1p+3onTUr9N/qevP3kSZPSmCOLeURpxJXl/5PWpOmSOtVzS1mPzeU6KfWq55aUc7pOSv3qcpPGaYYphtrdDEpd1O5mUOqidp9/R9+m1EPFM48oHf6I2m0eUeqhdptHlDqo3fYRpaOVcYDG278rtPH2ERr//MYb4htvGLLxvOp51fOq51U/PgAAAAAfGl0flQBGOTMAAAAASUVORK5CYII=" width="100" height="100">
<figcaption><strong>IMG</strong> static image</figcaption>
</figure>
<figure>
<canvas data-jdenticon-value="Icon04" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> data-jdenticon-value</figcaption>
</figure>
<figure>
<canvas id="canvas-Jdenticon-update-value" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> update(value)</figcaption>
</figure>
<figure>
<canvas id="canvas-Jdenticon-jquery-value" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> jQuery(value)</figcaption>
</figure>
<figure>
<canvas id="canvas-Jdenticon-drawIcon-value" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> drawIcon(value)</figcaption>
</figure>
<figure>
<svg data-jdenticon-value="Icon04" width="100" height="100"></svg>
<figcaption><strong>SVG</strong> data-jdenticon-value</figcaption>
</figure>
<figure>
<svg id="svg-Jdenticon-update-value" width="100" height="100"></svg>
<figcaption><strong>SVG</strong> update(value)</figcaption>
</figure>
<figure>
<svg id="svg-Jdenticon-jquery-value" width="100" height="100"></svg>
<figcaption><strong>SVG</strong> jQuery(value)</figcaption>
</figure>
<figure>
<span id="svg-Jdenticon-toSvg-value"></span>
<figcaption><strong>SVG</strong> toSvg(value)</figcaption>
</figure>
<script>
var padding = 0;
// Load padding from query string
var match = /\?padding=(\d+)/.exec(location.href);
if (match) {
padding = Number(match[1]);
}
document.getElementById("padding-percent").innerHTML = padding;
var className = document.body.className;
document.body.className = (className ? className + " " : "") + "padding-" + padding;
var jdenticon_config = {
padding: padding / 100,
backColor: "#00f1",
};
</script>
<script src="https://cdn.jsdelivr.net/jquery/3.2.1/jquery.slim.min.js"></script>
<script src="/node_modules/jdenticon/dist/jdenticon.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/require1k@1.0.1/require1k.min.js"></script>
<script>
jdenticon.update("#canvas-Jdenticon-update-value,#svg-Jdenticon-update-value", "Icon04");
var ctx = document.getElementById("canvas-Jdenticon-drawIcon-value").getContext("2d");
jdenticon.drawIcon(ctx, "Icon04", 100);
document.getElementById("svg-Jdenticon-toSvg-value").innerHTML =
jdenticon.toSvg("Icon04", 100);
$("#canvas-Jdenticon-jquery-value,#svg-Jdenticon-jquery-value").jdenticon("Icon04");
</script>
<script src="common.js"></script>
</body>
</html>

View File

@@ -0,0 +1,64 @@
html {
padding: 0;
margin: 0;
}
body {
margin: 0;
padding: 20px;
}
h1 {
font: bold 16px Arial;
margin: 30px 0 15px;
}
h1:first-child {
margin-top: 0;
}
.test-metadata {
font: 14px Arial;
margin: 0 0 1em;
min-height: 4em;
background: #C1D1EA;
padding: 10px;
line-height: 1.4;
}
.jdenticon-info {
font-weight: bold;
}
canvas,
svg,
img {
border: 6px solid #444;
margin-bottom: 5px;
}
figure {
width: 116px;
font: 10px Arial;
margin: 0 10px 16px 0;
display: inline-block;
vertical-align: top;
}
figure strong {
display: block;
}
.padding-0 .padding-30-only,
.padding-30 .padding-0-only {
display: none;
}
iframe {
border: none;
width: 100%;
height: 100px;
margin: 0 -20px;
}

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>Jdenticon</title>
<link rel="stylesheet" type="text/css" href="styles.css">
<script>
var jdenticon_config = {
backColor: "#00f1"
};
</script>
<script src="/node_modules/jdenticon/dist/jdenticon.min.js"></script>
</head>
<body>
<h1>"Icon0" - UMD in &lt;head&gt; - Should be equal</h1>
<figure>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAWlBMVEUAAP9aOuYxMYKdne+CXN5AQGLHx+lDQ2TIyOlDQ13NzedHR1ZHR1jW1ufY2OdLS1Dg4OVKSk/g4OWjc9Gjc9FLS02ldNDj4+RLS0yldNDj4+VMTEymddHl5eU2yqFLAAAAG3RSTlMRHy8vPVtbXFxra4iIiIjDw8TE0tPw8PDx8fEmYw1hAAABrUlEQVR42u2ZYWvCQBBE6542UVuT2Kaxnvf//2aR45hrl2mgxyKFfR+HjM9EggP35DiOY0x4fn0OP0PZveyEhLTOFWO6M367ToZ4ZxAe6jrnsKTMckC4n2Nm3tNQ1ylhSYUllFDmWJiFhrrOGBMYSzhEMNBQ1wkh1YQcSqwRErK6pk81fQ67WNORkNU1p1RzyuEx1hxJSOrmd/KA3wScEziXcIpgoqGuM8I1Fa54Ty6xcBEa6jrlcC0X1W/8pXzcnoe6zgnnfLOhDmXKj0VISOtc0596dY10x05oqOuO8xAwD2zrmAeEprqeB4Smup4HhJa6ngeMlrr+KyK01PWfKqOhTuaBYrNtqKuvQhxvn9uG+q8PFY7b7WP7x7qeB9TBLOt1PQ+Yg1nW63oeMAezrNf1POAObuF1Mg+Yg1t4ncMdsFDaHbBYOMwtm3ctwatvYYHDygKHwRKABQ6DIQELHO1DQlvgMBgSsMBhMSRggaN9SBBLdtgMCVjg+K9DAlgOCWA5JIDlkAA2Q4LTPiQ4JkMCrNcdx482/Gij5Whj8qMNP9rwow0/2nAcx7HjC8pg1fBM8ggRAAAAAElFTkSuQmCC" width="100" height="100">
<figcaption><strong>IMG</strong> static image</figcaption>
</figure>
<figure>
<canvas data-jdenticon-value="Icon0" width="100" height="100"></canvas>
<figcaption><strong>Canvas</strong> UMD in &lt;head&gt;</figcaption>
</figure>
<script src="common.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

View File

@@ -0,0 +1,45 @@
const { PNG } = require("pngjs");
async function screenshot(driver) {
var dimensions = await driver.executeScript(`return {
scrollWidth: document.body.offsetWidth,
scrollHeight: document.body.offsetHeight,
innerWidth: window.innerWidth || document.documentElement.clientWidth,
innerHeight: window.innerHeight || document.documentElement.clientHeight
}`);
const combinedImage = new PNG({
width: dimensions.scrollWidth,
height: dimensions.scrollHeight
});
const xnum = Math.ceil(dimensions.scrollWidth / dimensions.innerWidth);
const ynum = Math.ceil(dimensions.scrollHeight / dimensions.innerHeight);
for (let x = 0; x < xnum; x++) {
for (let y = 0; y < ynum; y++) {
var scrollpos = await driver.executeScript(`
window.scrollTo(${x * dimensions.innerWidth}, ${y * dimensions.innerHeight});
return { x: window.scrollX || window.pageXOffset, y: window.scrollY || window.pageYOffset }`)
// Delay for Safari
await driver.sleep(500);
const datauri = await driver.takeScreenshot();
const image = PNG.sync.read(Buffer.from(datauri, "base64"));
PNG.bitblt(image, combinedImage,
0, 0,
Math.min(image.width, combinedImage.width - scrollpos.x),
Math.min(image.height, combinedImage.height - scrollpos.y),
scrollpos.x,
scrollpos.y);
}
}
return PNG.sync.write(combinedImage);
}
module.exports = screenshot;

View File

@@ -0,0 +1,187 @@
const express = require("express");
const fs = require("fs");
const tap = require("tap");
const webdriver = require("selenium-webdriver");
const screenshot = require("./screenshooter");
const BlinkDiff = require("blink-diff");
const path = require("path");
// Command line arguments examples:
// node test.js win ie11,chrome
// node test.js macos safari,chrome,firefox
const environmentId = process.argv[2] || "";
const enabledBrowsers = (process.argv[3] || "").split(/[,;]/g).filter(name => name);
if (!enabledBrowsers.length) {
throw new Error("Expected browser names");
}
const screenshotDir = process.env.BROWSER_SCREENSHOT_DIR || path.join(__dirname, "artifacts/screenshots");
const diffDir = process.env.BROWSER_DIFF_DIR || path.join(__dirname, "artifacts/diffs");
const expectedDir = process.env.BROWSER_EXPECTED_DIR || path.join(__dirname, "expected");
// fs.mkdirSync(_ , { recursive: true }) did not work on GitHub Actions using Node v12.18.2 (Windows and macOS).
// Worked fine locally however. Replacing it with a custom recursive implementation.
// Ignored GitHub issue for tracking any status:
// https://github.com/nodejs/node/issues/27293
function mkdirRecursive(dirPath) {
if (!fs.existsSync(dirPath)) {
const parent = path.dirname(dirPath)
if (parent && parent !== dirPath) {
mkdirRecursive(parent);
}
fs.mkdirSync(dirPath);
}
}
mkdirRecursive(screenshotDir);
mkdirRecursive(diffDir);
const BROWSER_DEFINITIONS = [
{
name: "ie11",
uaCompatible: "IE=Edge",
capabilities: {
"browserName": webdriver.Browser.INTERNET_EXPLORER,
"ie.ensureCleanSession": true,
},
},
{
name: "ie10",
uaCompatible: "IE=10",
capabilities: {
"browserName": webdriver.Browser.INTERNET_EXPLORER,
"ie.ensureCleanSession": true,
},
},
{
name: "ie9",
uaCompatible: "IE=9",
capabilities: {
"browserName": webdriver.Browser.INTERNET_EXPLORER,
"ie.ensureCleanSession": true,
},
},
{
name: "firefox",
capabilities: {
"browserName": webdriver.Browser.FIREFOX,
},
},
{
name: "chrome",
capabilities: {
"browserName": webdriver.Browser.CHROME,
},
},
{
name: "edge",
capabilities: {
"browserName": webdriver.Browser.EDGE,
"ms:edgeChromium": true,
},
},
{
name: "safari",
capabilities: {
"browserName": webdriver.Browser.SAFARI,
},
},
]
async function serve(root, options, asyncCallback) {
const app = express();
app.use(express.static(root, options));
await new Promise((resolve, reject) => {
const listener = app.listen(async () => {
try {
await asyncCallback(listener);
resolve();
} catch (e) {
reject(e);
} finally {
listener.close();
}
});
});
}
async function testBrowser(browserName) {
const browser = BROWSER_DEFINITIONS.find(x => x.name === browserName);
await tap.test(browserName, async t => {
if (!browser) {
t.fail(`Could not find a browser with the name ${browserName}.`);
return;
}
await serve(
path.join(__dirname, "../../"),
{
"index": ["index.html"],
setHeaders: resp => {
// Prevent stale files
resp.setHeader("Cache-Control", "no-store");
if (browser.uaCompatible) {
resp.setHeader("X-UA-Compatible", browser.uaCompatible);
}
}
},
async listener => {
const url = "http://localhost:" + listener.address().port + "/e2e/browser/assets/";
console.log(`Screenshot in ${browserName}`);
console.log(url);
const driver = await new webdriver.Builder()
.withCapabilities(browser.capabilities)
.build();
await driver.manage().window().setRect({ width: 1000, height: 2000 });
const documentInitialised = () => driver.executeScript("return true");
try {
await driver.get(url);
await driver.wait(() => documentInitialised(), 10000);
await driver.sleep(2500);
const screenshotBuffer = await screenshot(driver);
fs.writeFileSync(path.join(screenshotDir, `${environmentId}-${browserName}.png`), screenshotBuffer);
} finally {
await driver.quit();
}
var diff = new BlinkDiff({
imageAPath: path.join(expectedDir, `${environmentId}-${browserName}.png`),
imageBPath: path.join(screenshotDir, `${environmentId}-${browserName}.png`),
thresholdType: BlinkDiff.THRESHOLD_PIXEL,
threshold: 1000,
imageOutputPath: path.join(diffDir, `${environmentId}-${browserName}.png`),
// Ignore test metadata area containing browser versions etc.
blockOut: [{ x: 0, y: 0, width: 20000, height: 100 }],
});
const diffResult = await diff.runWithPromise();
t.ok(diff.hasPassed(diffResult.code), `Found ${diffResult.differences} differences.`);
},
);
});
}
async function testBrowsers(enabledBrowsers) {
for (var i = 0; i < enabledBrowsers.length; i++) {
await testBrowser(enabledBrowsers[i]);
}
}
testBrowsers(enabledBrowsers);

Some files were not shown because too many files have changed in this diff Show More