gitter-markdown-processor
Advanced tools
Comparing version 14.0.0 to 15.0.0
@@ -1,9 +0,9 @@ | ||
"use strict"; | ||
'use strict'; | ||
var url = require('url'); | ||
const url = require('url'); | ||
var URL_RE = /(?:\/(.*))+?\/(.*)\/(issues|merge_requests|pull|commit)\/([a-f0-9]+)$/; | ||
const URL_RE = /(?:\/(.*))+?\/(.*)\/(issues|merge_requests|pull|commit)\/([a-f0-9]+)$/; | ||
function isValidIssueNumber(val) { | ||
var number = Number(val); | ||
const number = Number(val); | ||
return number > 0 && number < Infinity; | ||
@@ -13,3 +13,3 @@ } | ||
function isValidCommitHash(val) { | ||
var hash = val.match(/[a-f0-9]*/)[0] || ''; | ||
const hash = val.match(/[a-f0-9]*/)[0] || ''; | ||
return val.length === hash.length; | ||
@@ -27,24 +27,25 @@ } | ||
// https://gitlab.com/gitlab-org/gitter/styleguide/commit/6a61175e447548d9e1f3e5ed8e329d8578a38bb1 | ||
var urlObj = url.parse(href); | ||
var pathMatches = urlObj.pathname && urlObj.pathname.match(URL_RE); | ||
const urlObj = url.parse(href); | ||
const pathMatches = urlObj.pathname && urlObj.pathname.match(URL_RE); | ||
var isGitLab = urlObj.hostname === 'gitlab.com'; | ||
var isGitHub = urlObj.hostname === 'github.com'; | ||
const isGitLab = urlObj.hostname === 'gitlab.com'; | ||
const isGitHub = urlObj.hostname === 'github.com'; | ||
if((isGitHub || isGitLab) && !urlObj.hash && pathMatches) { | ||
if ((isGitHub || isGitLab) && !urlObj.hash && pathMatches) { | ||
const group = pathMatches[1]; | ||
const project = pathMatches[2]; | ||
const pathType = pathMatches[3]; | ||
const id = pathMatches[4]; | ||
var group = pathMatches[1]; | ||
var project = pathMatches[2]; | ||
var pathType = pathMatches[3]; | ||
var id = pathMatches[4]; | ||
if((pathType === 'issues' || pathType === 'merge_requests' || pathType === 'pull') && id && isValidIssueNumber(id)) { | ||
var type = 'issue'; | ||
if(project === 'issues') { | ||
if ( | ||
(pathType === 'issues' || pathType === 'merge_requests' || pathType === 'pull') && | ||
id && | ||
isValidIssueNumber(id) | ||
) { | ||
let type = 'issue'; | ||
if (project === 'issues') { | ||
type = 'issue'; | ||
} | ||
else if(pathType === 'merge_requests') { | ||
} else if (pathType === 'merge_requests') { | ||
type = 'mr'; | ||
} | ||
else if(pathType === 'pull') { | ||
} else if (pathType === 'pull') { | ||
type = 'pr'; | ||
@@ -54,21 +55,21 @@ } | ||
return { | ||
type: type, | ||
type, | ||
provider: isGitLab ? 'gitlab' : 'github', | ||
repo: group + '/' + project, | ||
id: id, | ||
href: href, | ||
text: group + '/' + project + (isGitLab && type === 'mr' ? '!' : '#') + id | ||
repo: `${group}/${project}`, | ||
id, | ||
href, | ||
text: `${group}/${project}${isGitLab && type === 'mr' ? '!' : '#'}${id}`, | ||
}; | ||
} else if(pathType === 'commit' && id && isValidCommitHash(id)) { | ||
} | ||
if (pathType === 'commit' && id && isValidCommitHash(id)) { | ||
return { | ||
type: 'commit', | ||
provider: isGitLab ? 'gitlab' : 'github', | ||
repo: group + '/' + project, | ||
id: id, | ||
href: href, | ||
text: group + '/' + project + '@' + id.substring(0,7) | ||
repo: `${group}/${project}`, | ||
id, | ||
href, | ||
text: `${group}/${project}@${id.substring(0, 7)}`, | ||
}; | ||
} | ||
} | ||
}; |
@@ -1,28 +0,26 @@ | ||
"use strict"; | ||
'use strict'; | ||
var cld = require('cld'); | ||
var Promise = require('bluebird'); | ||
const cld = require('cld'); | ||
const Promise = require('bluebird'); | ||
module.exports = exports = function detectLang(text) { | ||
return Promise.fromCallback(function(callback) { | ||
cld.detect(text, callback); | ||
}) | ||
.then(function(result) { | ||
if(!result || !result.languages || !Array.isArray(result.languages)) return; | ||
return Promise.fromCallback(callback => { | ||
cld.detect(text, callback); | ||
}) | ||
.then(result => { | ||
if (!result || !result.languages || !Array.isArray(result.languages)) return; | ||
// Sometimes there are undefined values in the array | ||
// Seems to be when the result is unreliable | ||
var langs = result.languages.filter(function(f) { | ||
return !!f; | ||
}); | ||
const langs = result.languages.filter(f => Boolean(f)); | ||
var primary = langs.shift(); | ||
const primary = langs.shift(); | ||
if(!primary) return; | ||
if (!primary) return; | ||
return primary.code; | ||
}) | ||
.catch(function() { | ||
return; // Ignore errors | ||
.catch(() => { | ||
// Ignore errors | ||
}); | ||
}; |
@@ -1,22 +0,19 @@ | ||
"use strict"; | ||
'use strict'; | ||
var Promise = require('bluebird'); | ||
var processChat = require('./process-chat'); | ||
var detectLang = require('./detect-lang'); | ||
const Promise = require('bluebird'); | ||
const processChat = require('./process-chat'); | ||
const detectLang = require('./detect-lang'); | ||
module.exports = exports = function processChatAsync(text, callback) { | ||
return Promise.try(function() { | ||
return processChat(text); | ||
}) | ||
.then(function(result) { | ||
var plainText = result.plainText.trim(); | ||
return Promise.try(() => processChat(text)) | ||
.then(result => { | ||
const plainText = result.plainText.trim(); | ||
if(!plainText) return result; | ||
return detectLang(plainText) | ||
.then(function(lang) { | ||
result.lang = lang; | ||
return result; | ||
}); | ||
if (!plainText) return result; | ||
return detectLang(plainText).then(lang => { | ||
result.lang = lang; | ||
return result; | ||
}); | ||
}) | ||
.nodeify(callback); | ||
}; |
@@ -1,131 +0,165 @@ | ||
"use strict"; | ||
'use strict'; | ||
var marked = require('gitter-marked'); | ||
var highlight = require('highlight.js'); | ||
var _ = require('underscore'); | ||
var util = require('util'); | ||
var katex = require('katex'); | ||
var matcher = require('./decoration-url-matcher'); | ||
var htmlencode = require('htmlencode'); | ||
const marked = require('gitter-marked'); | ||
const highlight = require('highlight.js'); | ||
const _ = require('underscore'); | ||
const util = require('util'); | ||
const katex = require('katex'); | ||
const htmlencode = require('htmlencode'); | ||
const matcher = require('./decoration-url-matcher'); | ||
var options = { gfm: true, tables: true, sanitize: true, breaks: true, linkify: true, skipComments: true }; | ||
const options = { | ||
gfm: true, | ||
tables: true, | ||
sanitize: true, | ||
breaks: true, | ||
linkify: true, | ||
skipComments: true, | ||
}; | ||
var lexer = new marked.Lexer(options); | ||
const lexer = new marked.Lexer(options); | ||
var JAVA = 'java'; | ||
var SCRIPT = 'script:'; | ||
var scriptUrl = JAVA + SCRIPT; | ||
var dataUrl = 'data:'; | ||
var httpUrl = 'http://'; | ||
var httpsUrl = 'https://'; | ||
var noProtocolUrl = '//'; | ||
highlight.configure({ | ||
classPrefix: '', | ||
languages: [ | ||
'apache', | ||
'applescript', | ||
'css', | ||
'bash', | ||
'clojure-repl', | ||
'clojure', | ||
'javascript', | ||
'coffeescript', | ||
'cpp', | ||
'cs', | ||
'd', | ||
'dart', | ||
'delphi', | ||
'diff', | ||
'django', | ||
'dockerfile', | ||
'dos', | ||
'elixir', | ||
'erb', | ||
'erlang-repl', | ||
'erlang', | ||
'fortran', | ||
'fsharp', | ||
'gcode', | ||
'gherkin', | ||
'go', | ||
'gradle', | ||
'groovy', | ||
'haml', | ||
'handlebars', | ||
'haskell', | ||
'http', | ||
'ini', | ||
'java', | ||
'json', | ||
'kotlin', | ||
'less', | ||
'lisp', | ||
'livescript', | ||
'lua', | ||
'makefile', | ||
'markdown', | ||
'mathematica', | ||
'matlab', | ||
'nginx', | ||
'objectivec', | ||
'perl', | ||
'php', | ||
'powershell', | ||
'prolog', | ||
'puppet', | ||
'python', | ||
'q', | ||
'r', | ||
'rib', | ||
'rsl', | ||
'ruby', | ||
'rust', | ||
'scala', | ||
'scheme', | ||
'scilab', | ||
'scss', | ||
'smali', | ||
'smalltalk', | ||
'sml', | ||
'sql', | ||
'stylus', | ||
'swift', | ||
'tcl', | ||
'tex', | ||
'typescript', | ||
'vbnet', | ||
'vbscript-html', | ||
'vbscript', | ||
'vim', | ||
'x86asm', | ||
'xml', | ||
], | ||
}); | ||
highlight.configure({ classPrefix: '', languages: [ | ||
"apache", | ||
"applescript", | ||
"css", | ||
"bash", | ||
"clojure-repl", | ||
"clojure", | ||
"javascript", | ||
"coffeescript", | ||
"cpp", | ||
"cs", | ||
"d", | ||
"dart", | ||
"delphi", | ||
"diff", | ||
"django", | ||
"dockerfile", | ||
"dos", | ||
"elixir", | ||
"erb", | ||
"erlang-repl", | ||
"erlang", | ||
"fortran", | ||
"fsharp", | ||
"gcode", | ||
"gherkin", | ||
"go", | ||
"gradle", | ||
"groovy", | ||
"haml", | ||
"handlebars", | ||
"haskell", | ||
"http", | ||
"ini", | ||
"java", | ||
"json", | ||
"kotlin", | ||
"less", | ||
"lisp", | ||
"livescript", | ||
"lua", | ||
"makefile", | ||
"markdown", | ||
"mathematica", | ||
"matlab", | ||
"nginx", | ||
"objectivec", | ||
"perl", | ||
"php", | ||
"powershell", | ||
"prolog", | ||
"puppet", | ||
"python", | ||
"q", | ||
"r", | ||
"rib", | ||
"rsl", | ||
"ruby", | ||
"rust", | ||
"scala", | ||
"scheme", | ||
"scilab", | ||
"scss", | ||
"smali", | ||
"smalltalk", | ||
"sml", | ||
"sql", | ||
"stylus", | ||
"swift", | ||
"tcl", | ||
"tex", | ||
"typescript", | ||
"vbnet", | ||
"vbscript-html", | ||
"vbscript", | ||
"vim", | ||
"x86asm", | ||
"xml" | ||
]}); | ||
const startsWith = (string, substring) => | ||
string | ||
.trim() | ||
.toLowerCase() | ||
.indexOf(substring) === 0; | ||
function checkForIllegalUrl(href) { | ||
if(!href) return ""; | ||
href = href.trim(); | ||
var hrefLower = href.toLowerCase(); | ||
if(hrefLower.indexOf(scriptUrl) === 0 || hrefLower.indexOf(dataUrl) === 0) { | ||
const replaceScriptUrls = urlString => { | ||
// eslint-disable-next-line no-script-url | ||
if (startsWith(urlString, 'javascript:') || startsWith(urlString, 'data:')) { | ||
/* Rickroll the script kiddies */ | ||
return "https://goo.gl/7NDM3x"; | ||
return 'https://goo.gl/7NDM3x'; | ||
} | ||
return urlString; | ||
}; | ||
if(hrefLower.indexOf(httpUrl) !== 0 && hrefLower.indexOf(httpsUrl) !== 0 && hrefLower.indexOf(noProtocolUrl) !== 0) { | ||
return httpUrl + href; | ||
/* prepend http protocol if URL doesn't use it yet */ | ||
const prependHttp = urlString => { | ||
if ( | ||
!startsWith(urlString, 'http://') && | ||
!startsWith(urlString, 'https://') && | ||
!startsWith(urlString, '//') | ||
) { | ||
return `http://${urlString}`; | ||
} | ||
return urlString; | ||
}; | ||
return href; | ||
/* use punycode version of url if it contains unicode */ | ||
const normalizeIdn = urlString => { | ||
const parsedUrl = new URL(urlString); | ||
if (startsWith(parsedUrl.host, 'xn--')) { | ||
return parsedUrl.href; | ||
} | ||
return urlString; | ||
}; | ||
const RTLO = '\u202E'; | ||
const ENCODED_RTLO = '%E2%80%AE'; | ||
/* replaces right to left override character */ | ||
const replaceRtlo = urlString => urlString.replace(RTLO, ENCODED_RTLO); | ||
function validateUrl(urlString) { | ||
if (!urlString) return ''; | ||
return [urlString] | ||
.map(replaceScriptUrls) | ||
.map(replaceRtlo) | ||
.map(prependHttp) | ||
.map(normalizeIdn) | ||
.pop(); | ||
} | ||
function getRenderer(renderContext) { | ||
const renderer = new marked.Renderer(); | ||
var renderer = new marked.Renderer(); | ||
// Highlight code blocks | ||
renderer.code = function(code, lang) { | ||
lang = (lang + '').toLowerCase(); | ||
lang = String(lang).toLowerCase(); | ||
if (lang === "text") { | ||
if (lang === 'text') { | ||
return util.format('<pre><code class="text">%s</code></pre>', htmlencode.htmlEncode(code)); | ||
@@ -135,3 +169,7 @@ } | ||
if (highlight.getLanguage(lang)) | ||
return util.format('<pre><code class="%s">%s</code></pre>', lang, highlight.highlight(lang, code).value); | ||
return util.format( | ||
'<pre><code class="%s">%s</code></pre>', | ||
lang, | ||
highlight.highlight(lang, code).value, | ||
); | ||
@@ -145,4 +183,8 @@ return util.format('<pre><code>%s</code></pre>', highlight.highlightAuto(code).value); | ||
return katex.renderToString(latexCode); | ||
} catch(e) { | ||
return util.format('<pre><code>%s: %s</code></pre>', htmlencode.htmlEncode(e.message), htmlencode.htmlEncode(latexCode)); | ||
} catch (e) { | ||
return util.format( | ||
'<pre><code>%s: %s</code></pre>', | ||
htmlencode.htmlEncode(e.message), | ||
htmlencode.htmlEncode(latexCode), | ||
); | ||
} | ||
@@ -160,18 +202,18 @@ }; | ||
number: issue, | ||
repo: repo ? repo : undefined | ||
repo: repo || undefined, | ||
}); | ||
var out = '<a target="_blank" data-link-type="' + type + '" data-issue="' + issue + '"'; | ||
if(href) { | ||
let out = `<a target="_blank" data-link-type="${type}" data-issue="${issue}"`; | ||
if (href) { | ||
out += util.format(' href="%s"', href); | ||
} | ||
if(provider) { | ||
if (provider) { | ||
out += util.format(' data-provider="%s"', provider); | ||
} | ||
if(repo) { | ||
if (repo) { | ||
out += util.format(' data-issue-repo="%s"', repo); | ||
} | ||
out += ' class="' + type + '">' + text + '</a>'; | ||
out += ` class="${type}">${text}</a>`; | ||
return out; | ||
} | ||
}; | ||
@@ -190,7 +232,7 @@ renderer.issue = function(provider, repo, issue, href, text) { | ||
renderer.commit = function(provider, repo, sha, href, text) { | ||
var text = repo+'@'+sha.substring(0, 7); | ||
renderer.commit = function(provider, repo, sha, href /* , text */) { | ||
const text = `${repo}@${sha.substring(0, 7)}`; | ||
if(!href) { | ||
var baseUrl = 'https://github.com/'; | ||
if (!href) { | ||
let baseUrl = 'https://github.com/'; | ||
if (provider === 'gitlab') { | ||
@@ -200,39 +242,54 @@ baseUrl = 'https://gitlab.com/'; | ||
href = baseUrl + repo + '/commit/' + sha; | ||
href = `${baseUrl + repo}/commit/${sha}`; | ||
} | ||
var out = '<a href="' + href + '" target="_blank" ' + | ||
'data-link-type="commit" ' + | ||
'data-provider="' + provider + '" ' + | ||
'data-commit-sha="' + sha + '" ' + | ||
'data-commit-repo="' + repo + '" ' + | ||
'class="commit">' + text + '</a>'; | ||
const out = | ||
`<a href="${href}" target="_blank" ` + | ||
`data-link-type="commit" ` + | ||
`data-provider="${provider}" ` + | ||
`data-commit-sha="${sha}" ` + | ||
`data-commit-repo="${repo}" ` + | ||
`class="commit">${text}</a>`; | ||
return out; | ||
}; | ||
renderer.link = function(href, title, text) { | ||
href = checkForIllegalUrl(href); | ||
var urlData = matcher(href); | ||
if(urlData) { | ||
return renderer[urlData.type](urlData.provider, urlData.repo, urlData.id, urlData.href, urlData.text); | ||
} else { | ||
renderContext.urls.push({ url: href }); | ||
return util.format('<a href="%s" rel="nofollow noopener noreferrer" target="_blank" class="link">%s</a>', href, text); | ||
renderer.link = (href, title, text) => { | ||
const validatedHref = validateUrl(href); | ||
const urlData = matcher(href); | ||
const showTooltip = validatedHref !== href ? 'link-tooltip' : ''; | ||
if (urlData) { | ||
return renderer[urlData.type]( | ||
urlData.provider, | ||
urlData.repo, | ||
urlData.id, | ||
urlData.href, | ||
urlData.text, | ||
); | ||
} | ||
renderContext.urls.push({ url: validatedHref }); | ||
return util.format( | ||
'<a href="%s" rel="nofollow noopener noreferrer" target="_blank" class="link %s">%s</a>', | ||
validatedHref, | ||
showTooltip, | ||
replaceRtlo(text), | ||
); | ||
}; | ||
renderer.image = function(href, title, text) { | ||
href = checkForIllegalUrl(href); | ||
href = validateUrl(href); | ||
renderContext.urls.push({ url: href }); | ||
if (title) { | ||
return util.format('<img src="%s" title="%s" alt="%s" rel="nofollow">', href, title, text); | ||
} else { | ||
return util.format('<img src="%s" alt="%s" rel="nofollow">', href, text); | ||
} | ||
return util.format('<img src="%s" alt="%s" rel="nofollow">', href, text); | ||
}; | ||
renderer.mention = function(href, title, text) { | ||
var screenName = text.charAt(0) === '@' ? text.substring(1) : text; | ||
renderContext.mentions.push({ screenName: screenName }); | ||
return util.format('<span data-link-type="mention" data-screen-name="%s" class="mention">%s</span>', screenName, text); | ||
const screenName = text.charAt(0) === '@' ? text.substring(1) : text; | ||
renderContext.mentions.push({ screenName }); | ||
return util.format( | ||
'<span data-link-type="mention" data-screen-name="%s" class="mention">%s</span>', | ||
screenName, | ||
text, | ||
); | ||
}; | ||
@@ -242,7 +299,11 @@ | ||
renderContext.mentions.push({ screenName: name, group: true }); | ||
return util.format('<span data-link-type="groupmention" data-group-name="%s" class="groupmention">%s</span>', name, text); | ||
return util.format( | ||
'<span data-link-type="groupmention" data-group-name="%s" class="groupmention">%s</span>', | ||
name, | ||
text, | ||
); | ||
}; | ||
renderer.email = function(href, title, text) { | ||
checkForIllegalUrl(href); | ||
validateUrl(href); | ||
@@ -253,10 +314,4 @@ renderContext.urls.push({ url: href }); | ||
renderer.heading = function(text, level/*, raw */) { | ||
return '<h' + | ||
level + | ||
'>' + | ||
text + | ||
'</h' + | ||
level + | ||
'>\n'; | ||
renderer.heading = function(text, level /* , raw */) { | ||
return `<h${level}>${text}</h${level}>\n`; | ||
}; | ||
@@ -273,7 +328,4 @@ | ||
module.exports = exports = function processChat(text) { | ||
var renderContext = { | ||
const renderContext = { | ||
urls: [], | ||
@@ -283,10 +335,10 @@ mentions: [], | ||
plainText: [], | ||
paragraphCount: 0 | ||
paragraphCount: 0, | ||
}; | ||
var html = ""; | ||
let html = ''; | ||
if(text) { | ||
text = "" + text; // Force to string | ||
var renderer = getRenderer(renderContext); | ||
if (text) { | ||
text = `${text}`; // Force to string | ||
const renderer = getRenderer(renderContext); | ||
// Reset any references, see https://github.com/gitterHQ/gitter/issues/1041 | ||
@@ -296,20 +348,20 @@ lexer.tokens = []; | ||
var tokens = lexer.lex(text); | ||
var parser = new marked.Parser(_.extend({ renderer: renderer }, options)); | ||
const tokens = lexer.lex(text); | ||
const parser = new marked.Parser(_.extend({ renderer }, options)); | ||
html = parser.parse(tokens); | ||
if(renderContext.paragraphCount === 1) { | ||
html = html.replace(/<\/?p>/g,''); | ||
if (renderContext.paragraphCount === 1) { | ||
html = html.replace(/<\/?p>/g, ''); | ||
} | ||
} else { | ||
text = ""; | ||
text = ''; | ||
} | ||
return { | ||
text: text, | ||
html: html, | ||
text, | ||
html, | ||
urls: renderContext.urls, | ||
mentions: renderContext.mentions, | ||
issues: renderContext.issues, | ||
plainText: renderContext.plainText.join(' ') | ||
plainText: renderContext.plainText.join(' '), | ||
}; | ||
}; |
@@ -1,14 +0,15 @@ | ||
"use strict"; | ||
'use strict'; | ||
var workerFarm = require('worker-farm'); | ||
var htmlencode = require('htmlencode'); | ||
var version = require('../package.json').version; | ||
const workerFarm = require('worker-farm'); | ||
const htmlencode = require('htmlencode'); | ||
const { version } = require('../package.json'); | ||
var Processor = function() { | ||
this.farm = workerFarm({ | ||
const Processor = function() { | ||
this.farm = workerFarm( | ||
{ | ||
maxConcurrentWorkers: 1, | ||
maxConcurrentCallsPerWorker: 1, | ||
maxCallTime: 3000 | ||
maxCallTime: 3000, | ||
}, | ||
require.resolve('./process-chat-async') | ||
require.resolve('./process-chat-async'), | ||
); | ||
@@ -18,11 +19,11 @@ }; | ||
Processor.prototype.process = function(text, callback) { | ||
this.farm(text, function(err, result) { | ||
if(err && err.type === 'TimeoutError') { | ||
this.farm(text, (err, result) => { | ||
if (err && err.type === 'TimeoutError') { | ||
result = { | ||
text: text, | ||
html: htmlencode.htmlEncode(text).replace(/\n| /g,'<br>'), | ||
text, | ||
html: htmlencode.htmlEncode(text).replace(/\n| /g, '<br>'), | ||
urls: [], | ||
mentions: [], | ||
issues: [], | ||
markdownProcessingFailed: true | ||
markdownProcessingFailed: true, | ||
}; | ||
@@ -29,0 +30,0 @@ } |
{ | ||
"name": "gitter-markdown-processor", | ||
"version": "14.0.0", | ||
"version": "15.0.0", | ||
"description": "parses gitter chat messages, but in its own process", | ||
@@ -18,5 +18,7 @@ "main": "index.js", | ||
"devDependencies": { | ||
"eslint": "^5.5.0", | ||
"@gitlab/eslint-config": "^1.6.0", | ||
"eslint": "^6.1.0", | ||
"eslint-plugin-node": "^7.0.1", | ||
"mocha": "^5.2.0" | ||
"mocha": "^5.2.0", | ||
"prettier": "^1.18.2" | ||
}, | ||
@@ -26,3 +28,4 @@ "scripts": { | ||
"mocha": "mocha", | ||
"lint": "eslint ." | ||
"validate": "eslint . && prettier --check **/*.js", | ||
"validate-fix": "eslint --fix . && prettier --write **/*.js" | ||
}, | ||
@@ -29,0 +32,0 @@ "author": "Andy Trevorah", |
@@ -1,13 +0,13 @@ | ||
/*jslint node:true, unused:true*/ | ||
/*global describe:true, it:true */ | ||
"use strict"; | ||
/* jslint node:true, unused:true */ | ||
/* global describe:true, it:true */ | ||
var assert = require('assert'); | ||
var matcher = require('../lib/decoration-url-matcher'); | ||
'use strict'; | ||
describe('decoration-url-matcher', function() { | ||
const assert = require('assert'); | ||
const matcher = require('../lib/decoration-url-matcher'); | ||
describe('GitLab', function() { | ||
it('should match an issue url', function() { | ||
var result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/issues/216'); | ||
describe('decoration-url-matcher', () => { | ||
describe('GitLab', () => { | ||
it('should match an issue url', () => { | ||
const result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/issues/216'); | ||
assert.equal(result.type, 'issue'); | ||
@@ -20,4 +20,4 @@ assert.equal(result.provider, 'gitlab'); | ||
it('should match an issue url in a sub-group', function() { | ||
var result = matcher('https://gitlab.com/gitlab-org/gitter/webapp/issues/1755'); | ||
it('should match an issue url in a sub-group', () => { | ||
const result = matcher('https://gitlab.com/gitlab-org/gitter/webapp/issues/1755'); | ||
assert.equal(result.type, 'issue'); | ||
@@ -30,9 +30,9 @@ assert.equal(result.provider, 'gitlab'); | ||
it('shouldnt match an issue url with non-numeric ID', function() { | ||
var result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/issues/abc'); | ||
it('shouldnt match an issue url with non-numeric ID', () => { | ||
const result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/issues/abc'); | ||
assert(!result); | ||
}); | ||
it('should match a merge request url', function() { | ||
var result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1'); | ||
it('should match a merge request url', () => { | ||
const result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1'); | ||
assert.equal(result.type, 'mr'); | ||
@@ -45,9 +45,11 @@ assert.equal(result.provider, 'gitlab'); | ||
it('shouldnt match a merge request url without ID', function() { | ||
var result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/'); | ||
it('shouldnt match a merge request url without ID', () => { | ||
const result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/'); | ||
assert(!result); | ||
}); | ||
it('should match a commit url', function() { | ||
var result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/commit/eb9ca0e7e1ea4c2151abc320199e844f794bda54'); | ||
it('should match a commit url', () => { | ||
const result = matcher( | ||
'https://gitlab.com/gitlab-org/gitlab-ce/commit/eb9ca0e7e1ea4c2151abc320199e844f794bda54', | ||
); | ||
assert.equal(result.type, 'commit'); | ||
@@ -60,14 +62,14 @@ assert.equal(result.provider, 'gitlab'); | ||
it('shouldnt match an odd commit url', function() { | ||
var result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/commit/xxxxxxxxxxxx'); | ||
it('shouldnt match an odd commit url', () => { | ||
const result = matcher('https://gitlab.com/gitlab-org/gitlab-ce/commit/xxxxxxxxxxxx'); | ||
assert(!result); | ||
}); | ||
it('shouldnt match url with no path (host/port only)', function() { | ||
var result = matcher('localhost:1234'); | ||
it('shouldnt match url with no path (host/port only)', () => { | ||
const result = matcher('localhost:1234'); | ||
assert(!result); | ||
}); | ||
it('shouldnt match url with no path (no URL)', function() { | ||
var result = matcher(''); | ||
it('shouldnt match url with no path (no URL)', () => { | ||
const result = matcher(''); | ||
assert(!result); | ||
@@ -77,5 +79,5 @@ }); | ||
describe('GitHub', function() { | ||
it('should match an issue url', function() { | ||
var result = matcher('https://github.com/gitterHQ/gitter/issues/216'); | ||
describe('GitHub', () => { | ||
it('should match an issue url', () => { | ||
const result = matcher('https://github.com/gitterHQ/gitter/issues/216'); | ||
assert.equal(result.type, 'issue'); | ||
@@ -88,4 +90,4 @@ assert.equal(result.provider, 'github'); | ||
it('should match a pull request url', function() { | ||
var result = matcher('https://github.com/gitterHQ/gitter/pull/1'); | ||
it('should match a pull request url', () => { | ||
const result = matcher('https://github.com/gitterHQ/gitter/pull/1'); | ||
assert.equal(result.type, 'pr'); | ||
@@ -98,14 +100,18 @@ assert.equal(result.provider, 'github'); | ||
it('shouldnt match an odd japanese issue url', function() { | ||
var result = matcher('https://github.com/gitterHQ/gitter/issues/460]をマージしてもよろしいでしょうか?'); | ||
it('shouldnt match an odd japanese issue url', () => { | ||
const result = matcher( | ||
'https://github.com/gitterHQ/gitter/issues/460]をマージしてもよろしいでしょうか?', | ||
); | ||
assert(!result); | ||
}); | ||
it('shouldnt match an odd issue url', function() { | ||
var result = matcher('https://github.com/gitterHQ/gitter/issues/214]p'); | ||
it('shouldnt match an odd issue url', () => { | ||
const result = matcher('https://github.com/gitterHQ/gitter/issues/214]p'); | ||
assert(!result); | ||
}); | ||
it('should match a commit url', function() { | ||
var result = matcher('https://github.com/twbs/bootstrap/commit/c8a8e768510cc1bd9e72d5cade23fba715efb59f'); | ||
it('should match a commit url', () => { | ||
const result = matcher( | ||
'https://github.com/twbs/bootstrap/commit/c8a8e768510cc1bd9e72d5cade23fba715efb59f', | ||
); | ||
assert.equal(result.type, 'commit'); | ||
@@ -118,9 +124,9 @@ assert.equal(result.provider, 'github'); | ||
it('shouldnt match an odd commit url', function() { | ||
var result = matcher('https://github.com/gitterHQ/gitter/commit/xxxxxxxxxxxx'); | ||
it('shouldnt match an odd commit url', () => { | ||
const result = matcher('https://github.com/gitterHQ/gitter/commit/xxxxxxxxxxxx'); | ||
assert(!result); | ||
}); | ||
it('shouldnt match an odd commit url with no hash', function() { | ||
var result = matcher('https://github.com/gitterHQ/gitter/commit/'); | ||
it('shouldnt match an odd commit url with no hash', () => { | ||
const result = matcher('https://github.com/gitterHQ/gitter/commit/'); | ||
assert(!result); | ||
@@ -127,0 +133,0 @@ }); |
@@ -1,96 +0,78 @@ | ||
"use strict"; | ||
'use strict'; | ||
var assert = require('assert'); | ||
var processChatAsync = require('../lib/process-chat-async'); | ||
var fs = require('fs'); | ||
var path = require('path'); | ||
const assert = require('assert'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const processChatAsync = require('../lib/process-chat-async'); | ||
describe('process-chat-async', function() { | ||
describe('process-chat-async', () => { | ||
const dir = path.join(__dirname, 'markdown-conversions'); | ||
var dir = path.join(__dirname, 'markdown-conversions'); | ||
const items = fs.readdirSync(dir); | ||
items | ||
.filter(file => /\.markdown$/.test(file)) | ||
.forEach(file => { | ||
const markdownFile = path.join(dir, file); | ||
const htmlFile = markdownFile.replace('.markdown', '.html'); | ||
const markdown = fs.readFileSync(markdownFile, { encoding: 'utf8' }); | ||
const expectedHtml = fs.readFileSync(htmlFile, { encoding: 'utf8' }); | ||
var items = fs.readdirSync(dir); | ||
items.filter(function(file) { | ||
return /\.markdown$/.test(file); | ||
}).forEach(function(file) { | ||
var markdownFile = path.join(dir, file); | ||
var htmlFile = markdownFile.replace('.markdown', '.html'); | ||
var markdown = fs.readFileSync(markdownFile, { encoding: 'utf8' }); | ||
var expectedHtml = fs.readFileSync(htmlFile, { encoding: 'utf8' }); | ||
it('should handle ' + file, function() { | ||
return processChatAsync(markdown) | ||
.then(function(result) { | ||
var html = result.html; | ||
it(`should handle ${file}`, () => | ||
processChatAsync(markdown).then(result => { | ||
const { html } = result; | ||
assert.equal(html.trim(), expectedHtml.trim()); | ||
}); | ||
})); | ||
}); | ||
}); | ||
it('should detect japanese', () => | ||
processChatAsync('世界こんにちは、お元気ですか?').then(result => { | ||
assert.equal(result.lang, 'ja'); | ||
})); | ||
it('should detect japanese', function() { | ||
return processChatAsync("世界こんにちは、お元気ですか?") | ||
.then(function(result) { | ||
assert.equal(result.lang, 'ja'); | ||
}); | ||
}); | ||
it('should detect korean', () => | ||
processChatAsync('세계 안녕하세요, 어떻게 지내 ?').then(result => { | ||
assert.equal(result.lang, 'ko'); | ||
})); | ||
it('should detect korean', function() { | ||
return processChatAsync("세계 안녕하세요, 어떻게 지내 ?") | ||
.then(function(result) { | ||
assert.equal(result.lang, 'ko'); | ||
}); | ||
}); | ||
it('should detect russian', () => | ||
processChatAsync('Привет мир , как ты?').then(result => { | ||
assert.equal(result.lang, 'ru'); | ||
return processChatAsync('1. Привет мир , как ты?'); | ||
})); | ||
it('should detect russian', function() { | ||
return processChatAsync("Привет мир , как ты?") | ||
.then(function(result) { | ||
assert.equal(result.lang, 'ru'); | ||
return processChatAsync("1. Привет мир , как ты?"); | ||
}) | ||
}); | ||
it('should detect chinese (simplified)', () => | ||
processChatAsync('您好,欢迎来到小胶质').then(result => { | ||
assert.equal(result.lang, 'zh'); | ||
})); | ||
it('should detect chinese (simplified)', function() { | ||
return processChatAsync("您好,欢迎来到小胶质") | ||
.then(function(result) { | ||
assert.equal(result.lang, 'zh'); | ||
}); | ||
}); | ||
it('should detect chinese (traditional)', () => | ||
processChatAsync('您好,歡迎來到小膠質').then(result => { | ||
assert.equal(result.lang, 'zh-Hant'); | ||
})); | ||
it('should detect chinese (traditional)', function() { | ||
return processChatAsync("您好,歡迎來到小膠質") | ||
.then(function(result) { | ||
assert.equal(result.lang, 'zh-Hant'); | ||
}); | ||
}); | ||
it('should detect afrikaans', function() { | ||
return processChatAsync("hoe is jy meneer?") | ||
.then(function(result) { | ||
it('should detect afrikaans', () => | ||
processChatAsync('hoe is jy meneer?') | ||
.then(result => { | ||
assert.equal(result.lang, 'af'); | ||
return processChatAsync("## hoe is jy meneer?"); | ||
return processChatAsync('## hoe is jy meneer?'); | ||
}) | ||
.then(function(result) { | ||
.then(result => { | ||
assert.equal(result.lang, 'af'); | ||
}); | ||
}); | ||
})); | ||
it('should deal with unreliable text snippets', function() { | ||
return processChatAsync("あ、app/assets/javascripts/main.js は requirejs.config なんですか") | ||
.then(function(result) { | ||
it('should deal with unreliable text snippets', () => | ||
processChatAsync('あ、app/assets/javascripts/main.js は requirejs.config なんですか').then( | ||
result => { | ||
assert.equal(result.lang, 'ja'); | ||
}); | ||
}); | ||
}, | ||
)); | ||
it('should handle greek', function() { | ||
return processChatAsync("Μουλιάζουμε τα ξερά σύκα στο κρασί. Ζεσταίνουμε σε τηγάνι τη 1 κουτ. σούπας λάδι και σοτάρουμε το μπέικον, μέχρι να ροδίσει. Αλατοπιπερώνουμε και ρίχνουμε το χυμό λεμονιού,το υπόλοιπο λάδι και το σπανάκι. Ανακατεύουμε ίσα να λαδωθεί το σπανάκι και να μαραθεί λίγο. Στραγγίζουμε τα σύκα και τα ανακατεύουμε με το μείγμα του τηγανιού. Απλώνουμε τη σαλάτα πάνω στις φρυγανισμένες φέτες ψωμί και σερβίρουμε") | ||
.then(function(result) { | ||
assert.equal(result.lang, 'el'); | ||
}); | ||
}); | ||
it('should handle greek', () => | ||
processChatAsync( | ||
'Μουλιάζουμε τα ξερά σύκα στο κρασί. Ζεσταίνουμε σε τηγάνι τη 1 κουτ. σούπας λάδι και σοτάρουμε το μπέικον, μέχρι να ροδίσει. Αλατοπιπερώνουμε και ρίχνουμε το χυμό λεμονιού,το υπόλοιπο λάδι και το σπανάκι. Ανακατεύουμε ίσα να λαδωθεί το σπανάκι και να μαραθεί λίγο. Στραγγίζουμε τα σύκα και τα ανακατεύουμε με το μείγμα του τηγανιού. Απλώνουμε τη σαλάτα πάνω στις φρυγανισμένες φέτες ψωμί και σερβίρουμε', | ||
).then(result => { | ||
assert.equal(result.lang, 'el'); | ||
})); | ||
it('should handle null', function() { | ||
return processChatAsync(null); | ||
}); | ||
it('should handle null', () => processChatAsync(null)); | ||
}); |
@@ -1,39 +0,36 @@ | ||
"use strict"; | ||
'use strict'; | ||
var assert = require('assert'); | ||
var Processor = require('..'); | ||
var fs = require('fs'); | ||
var path = require('path'); | ||
const assert = require('assert'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const Processor = require('..'); | ||
function listTestPairs() { | ||
var dir = path.join(__dirname, 'markdown-conversions'); | ||
const dir = path.join(__dirname, 'markdown-conversions'); | ||
var items = fs.readdirSync(dir); | ||
return items.filter(function(file) { | ||
return /\.markdown$/.test(file); | ||
}).map(function(file) { | ||
var markdownFile = path.join(dir, file); | ||
var name = file.replace('.markdown', ''); | ||
var htmlFile = markdownFile.replace('.markdown', '.html'); | ||
var markdown = fs.readFileSync(markdownFile, { encoding: 'utf8' }); | ||
var expectedHtml = fs.readFileSync(htmlFile, { encoding: 'utf8' }); | ||
const items = fs.readdirSync(dir); | ||
return items | ||
.filter(file => /\.markdown$/.test(file)) | ||
.map(file => { | ||
const markdownFile = path.join(dir, file); | ||
const name = file.replace('.markdown', ''); | ||
const htmlFile = markdownFile.replace('.markdown', '.html'); | ||
const markdown = fs.readFileSync(markdownFile, { encoding: 'utf8' }); | ||
const expectedHtml = fs.readFileSync(htmlFile, { encoding: 'utf8' }); | ||
return { | ||
name: name, | ||
markdownFile: markdownFile, | ||
htmlFile: htmlFile, | ||
markdown: markdown, | ||
expectedHtml: expectedHtml | ||
}; | ||
}); | ||
return { | ||
name, | ||
markdownFile, | ||
htmlFile, | ||
markdown, | ||
expectedHtml, | ||
}; | ||
}); | ||
} | ||
describe('process-chat', function() { | ||
var processor = new Processor(); | ||
describe('process-chat', () => { | ||
const processor = new Processor(); | ||
after(function(callback) { | ||
processor.shutdown(function() { | ||
after(callback => { | ||
processor.shutdown(() => { | ||
// Add an extra time on cos mocha will just exit without waiting | ||
@@ -45,9 +42,9 @@ // for the child to shutdown | ||
describe('tests', function() { | ||
listTestPairs().forEach(function(item) { | ||
it('should handle ' + item.name, function(done) { | ||
processor.process(item.markdown, function(err, result) { | ||
if(err) return done(err); | ||
describe('tests', () => { | ||
listTestPairs().forEach(item => { | ||
it(`should handle ${item.name}`, done => { | ||
processor.process(item.markdown, (err, result) => { | ||
if (err) return done(err); | ||
var html = result.html; | ||
const { html } = result; | ||
assert.equal(html.trim(), item.expectedHtml.trim()); | ||
@@ -60,23 +57,20 @@ done(); | ||
describe.skip('performance tests', function() { | ||
listTestPairs().forEach(function(item) { | ||
it('should handle ' + item.name, function(done) { | ||
var completed = 0; | ||
for(var i = 0; i < 1000; i++) { | ||
processor.process(item.markdown, function(err, result) { | ||
describe.skip('performance tests', () => { | ||
listTestPairs().forEach(item => { | ||
it(`should handle ${item.name}`, done => { | ||
let completed = 0; | ||
for (let i = 0; i < 1000; i++) { | ||
processor.process(item.markdown, (err, result) => { | ||
completed++; | ||
if(err) return done(err); | ||
if (err) return done(err); | ||
var html = result.html; | ||
const { html } = result; | ||
assert.equal(html.trim(), item.expectedHtml.trim()); | ||
if(completed === 1000) return done(); | ||
if (completed === 1000) return done(); | ||
}); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -1,37 +0,117 @@ | ||
"use strict"; | ||
'use strict'; | ||
var assert = require('assert'); | ||
var processChat = require('../lib/process-chat'); | ||
var fs = require('fs'); | ||
var path = require('path'); | ||
const assert = require('assert'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const processChat = require('../lib/process-chat'); | ||
describe('process-chat', function() { | ||
describe('process-chat', () => { | ||
const dir = path.join(__dirname, 'markdown-conversions'); | ||
var dir = path.join(__dirname, 'markdown-conversions'); | ||
var items = fs.readdirSync(dir); | ||
items.filter(function(file) { | ||
return /\.markdown$/.test(file); | ||
}).forEach(function(file) { | ||
var markdownFile = path.join(dir, file); | ||
var htmlFile = markdownFile.replace('.markdown', '.html'); | ||
var markdown = fs.readFileSync(markdownFile, { encoding: 'utf8' }); | ||
var expectedHtml = fs.readFileSync(htmlFile, { encoding: 'utf8' }); | ||
it('should handle ' + file, function() { | ||
var html = processChat(markdown).html; | ||
assert.equal(html.trim(), expectedHtml.trim()); | ||
const items = fs.readdirSync(dir); | ||
items | ||
.filter(file => /\.markdown$/.test(file)) | ||
.forEach(file => { | ||
const markdownFile = path.join(dir, file); | ||
const htmlFile = markdownFile.replace('.markdown', '.html'); | ||
const markdown = fs.readFileSync(markdownFile, { encoding: 'utf8' }); | ||
const expectedHtml = fs.readFileSync(htmlFile, { encoding: 'utf8' }); | ||
it(`should handle ${file}`, () => { | ||
const { html } = processChat(markdown); | ||
assert.equal(html.trim(), expectedHtml.trim()); | ||
}); | ||
}); | ||
}); | ||
it('should isolate link references between messages', function() { | ||
var inputMd1 = '[Community for developers to chat][1]\n\n[1]: https://gitter.im/'; | ||
var inputMd2 = 'arr[1]'; | ||
var html1 = processChat(inputMd1).html; | ||
assert.equal(html1.trim(), '<a href="https://gitter.im/" rel="nofollow noopener noreferrer" target="_blank" class="link">Community for developers to chat</a>'); | ||
var html2 = processChat(inputMd2).html; | ||
it('should isolate link references between messages', () => { | ||
const inputMd1 = '[Community for developers to chat][1]\n\n[1]: https://gitter.im/'; | ||
const inputMd2 = 'arr[1]'; | ||
const html1 = processChat(inputMd1).html; | ||
assert.equal( | ||
html1.trim(), | ||
'<a href="https://gitter.im/" rel="nofollow noopener noreferrer" target="_blank" class="link ">Community for developers to chat</a>', | ||
); | ||
const html2 = processChat(inputMd2).html; | ||
assert.equal(html2.trim(), 'arr[1]'); | ||
}); | ||
describe('invalid and suspicious links', () => { | ||
it('replaces URL with javascript: and data:', () => { | ||
const examples = [ | ||
'[click here](data:text/html;base64,PHNjcmlwdD5hbGVydCgiSGVsbG8iKTs8L3NjcmlwdD4=)', | ||
// eslint-disable-next-line no-script-url | ||
'[click here](javascript:alert(1))', | ||
]; | ||
const htmlLinks = examples.map(markdown => processChat(markdown).html); | ||
htmlLinks.forEach(link => | ||
assert( | ||
link.indexOf('https://goo.gl/7NDM3x') !== -1, | ||
`should replace their link (${link}) with a rickroll video`, | ||
), | ||
); | ||
}); | ||
it('adds http to links without correct protocol', () => { | ||
const htmlLink = processChat('[label](www.example.com)').html; | ||
assert( | ||
htmlLink.indexOf('http://www.example.com') !== -1, | ||
`Link ${htmlLink} should have http:// prepended`, | ||
); | ||
}); | ||
it('should add a tooltip class to links that are not normalized/valid', () => { | ||
const examples = [ | ||
'http://example.com/evil\u202E3pm.exe', | ||
'[evilexe.mp3](http://example.com/evil\u202E3pm.exe)', | ||
'rdar://localhost.com/\u202E3pm.exe', | ||
'http://one😄two.com', | ||
'[Evil-Test](http://one😄two.com)', | ||
'http://\u0261itlab.com', | ||
'[Evil-GitLab-link](http://\u0261itlab.com)', | ||
]; | ||
const htmlLinks = examples.map(markdown => processChat(markdown).html); | ||
htmlLinks.forEach(link => | ||
assert(link.indexOf('tooltip') !== -1, `Link ${link} is missing tooltip`), | ||
); | ||
}); | ||
it('should use normalized href for IDN hosts', () => { | ||
const idnLink = 'http://one😄two.com'; | ||
const link = processChat(idnLink).html; | ||
// Test the displayed URL text in the <a>xxx</a> tag | ||
assert(link.indexOf('>http://one😄two.com<') !== -1, `Link label got changed: ${link}`); | ||
// Test the href attribute | ||
assert( | ||
link.indexOf('href="http://xn--onetwo-yw74e.com/"') !== -1, | ||
`Link href should be in punycode: ${link}`, | ||
); | ||
}); | ||
it('escapes RTLO characters', () => { | ||
// Rendered text looks like it does not have any extra RTLO characters in it - "http://example.com/evilexe.mp3" | ||
// Actual dodgy string with RTLO character in it - http://example.com/evil3pm.exe | ||
const evilLink = 'http://example.com/evil\u202E3pm.exe'; | ||
const link = processChat(evilLink).html; | ||
assert( | ||
link.indexOf('http://example.com/evil%E2%80%AE3pm.exe') !== -1, | ||
`RTLO character is not escaped: ${link}`, | ||
); | ||
assert( | ||
link.indexOf('\u202E') === -1, | ||
`Doesn't leave any unescaped RTLO characters anywhere in the link ${link}`, | ||
); | ||
}); | ||
it('should add no tooltips for safe links', () => { | ||
const goodExamples = [ | ||
'http://example.com', | ||
'[Safe-Test](http://example.com)', | ||
'https://commons.wikimedia.org/wiki/File:اسكرام_2_-_تمنراست.jpg', | ||
'[Wikipedia-link](https://commons.wikimedia.org/wiki/File:اسكرام_2_-_تمنراست.jpg)', | ||
]; | ||
const htmlLinks = goodExamples.map(markdown => processChat(markdown).html); | ||
htmlLinks.forEach(link => | ||
assert(link.indexOf('tooltip') === -1, `Link ${link} shouldn't have a tooltip`), | ||
); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
43892
38
820
5
2