Replace CDN React/ReactDOM/Babel with local libs; remove Babel and inline scripts Build Tailwind locally, add safelist; switch to assets/tailwind.css Self-host Font Awesome and Inter (CSS + woff2); remove external font CDNs Implement strict CSP (no unsafe-inline/eval; scripts/styles/fonts from self) Extract inline handlers; move PWA scripts to external files Add local QR code generation (qrcode lib) and remove api.qrserver.com Improve SessionTypeSelector visual selection (highlighted background and ring) Keep PWA working with service worker and offline assets Refs: CSP hardening, offline-first, no external dependencies
331 lines
9.1 KiB
JavaScript
331 lines
9.1 KiB
JavaScript
const Mode = require('./mode')
|
|
const NumericData = require('./numeric-data')
|
|
const AlphanumericData = require('./alphanumeric-data')
|
|
const ByteData = require('./byte-data')
|
|
const KanjiData = require('./kanji-data')
|
|
const Regex = require('./regex')
|
|
const Utils = require('./utils')
|
|
const dijkstra = require('dijkstrajs')
|
|
|
|
/**
|
|
* Returns UTF8 byte length
|
|
*
|
|
* @param {String} str Input string
|
|
* @return {Number} Number of byte
|
|
*/
|
|
function getStringByteLength (str) {
|
|
return unescape(encodeURIComponent(str)).length
|
|
}
|
|
|
|
/**
|
|
* Get a list of segments of the specified mode
|
|
* from a string
|
|
*
|
|
* @param {Mode} mode Segment mode
|
|
* @param {String} str String to process
|
|
* @return {Array} Array of object with segments data
|
|
*/
|
|
function getSegments (regex, mode, str) {
|
|
const segments = []
|
|
let result
|
|
|
|
while ((result = regex.exec(str)) !== null) {
|
|
segments.push({
|
|
data: result[0],
|
|
index: result.index,
|
|
mode: mode,
|
|
length: result[0].length
|
|
})
|
|
}
|
|
|
|
return segments
|
|
}
|
|
|
|
/**
|
|
* Extracts a series of segments with the appropriate
|
|
* modes from a string
|
|
*
|
|
* @param {String} dataStr Input string
|
|
* @return {Array} Array of object with segments data
|
|
*/
|
|
function getSegmentsFromString (dataStr) {
|
|
const numSegs = getSegments(Regex.NUMERIC, Mode.NUMERIC, dataStr)
|
|
const alphaNumSegs = getSegments(Regex.ALPHANUMERIC, Mode.ALPHANUMERIC, dataStr)
|
|
let byteSegs
|
|
let kanjiSegs
|
|
|
|
if (Utils.isKanjiModeEnabled()) {
|
|
byteSegs = getSegments(Regex.BYTE, Mode.BYTE, dataStr)
|
|
kanjiSegs = getSegments(Regex.KANJI, Mode.KANJI, dataStr)
|
|
} else {
|
|
byteSegs = getSegments(Regex.BYTE_KANJI, Mode.BYTE, dataStr)
|
|
kanjiSegs = []
|
|
}
|
|
|
|
const segs = numSegs.concat(alphaNumSegs, byteSegs, kanjiSegs)
|
|
|
|
return segs
|
|
.sort(function (s1, s2) {
|
|
return s1.index - s2.index
|
|
})
|
|
.map(function (obj) {
|
|
return {
|
|
data: obj.data,
|
|
mode: obj.mode,
|
|
length: obj.length
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Returns how many bits are needed to encode a string of
|
|
* specified length with the specified mode
|
|
*
|
|
* @param {Number} length String length
|
|
* @param {Mode} mode Segment mode
|
|
* @return {Number} Bit length
|
|
*/
|
|
function getSegmentBitsLength (length, mode) {
|
|
switch (mode) {
|
|
case Mode.NUMERIC:
|
|
return NumericData.getBitsLength(length)
|
|
case Mode.ALPHANUMERIC:
|
|
return AlphanumericData.getBitsLength(length)
|
|
case Mode.KANJI:
|
|
return KanjiData.getBitsLength(length)
|
|
case Mode.BYTE:
|
|
return ByteData.getBitsLength(length)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merges adjacent segments which have the same mode
|
|
*
|
|
* @param {Array} segs Array of object with segments data
|
|
* @return {Array} Array of object with segments data
|
|
*/
|
|
function mergeSegments (segs) {
|
|
return segs.reduce(function (acc, curr) {
|
|
const prevSeg = acc.length - 1 >= 0 ? acc[acc.length - 1] : null
|
|
if (prevSeg && prevSeg.mode === curr.mode) {
|
|
acc[acc.length - 1].data += curr.data
|
|
return acc
|
|
}
|
|
|
|
acc.push(curr)
|
|
return acc
|
|
}, [])
|
|
}
|
|
|
|
/**
|
|
* Generates a list of all possible nodes combination which
|
|
* will be used to build a segments graph.
|
|
*
|
|
* Nodes are divided by groups. Each group will contain a list of all the modes
|
|
* in which is possible to encode the given text.
|
|
*
|
|
* For example the text '12345' can be encoded as Numeric, Alphanumeric or Byte.
|
|
* The group for '12345' will contain then 3 objects, one for each
|
|
* possible encoding mode.
|
|
*
|
|
* Each node represents a possible segment.
|
|
*
|
|
* @param {Array} segs Array of object with segments data
|
|
* @return {Array} Array of object with segments data
|
|
*/
|
|
function buildNodes (segs) {
|
|
const nodes = []
|
|
for (let i = 0; i < segs.length; i++) {
|
|
const seg = segs[i]
|
|
|
|
switch (seg.mode) {
|
|
case Mode.NUMERIC:
|
|
nodes.push([seg,
|
|
{ data: seg.data, mode: Mode.ALPHANUMERIC, length: seg.length },
|
|
{ data: seg.data, mode: Mode.BYTE, length: seg.length }
|
|
])
|
|
break
|
|
case Mode.ALPHANUMERIC:
|
|
nodes.push([seg,
|
|
{ data: seg.data, mode: Mode.BYTE, length: seg.length }
|
|
])
|
|
break
|
|
case Mode.KANJI:
|
|
nodes.push([seg,
|
|
{ data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) }
|
|
])
|
|
break
|
|
case Mode.BYTE:
|
|
nodes.push([
|
|
{ data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) }
|
|
])
|
|
}
|
|
}
|
|
|
|
return nodes
|
|
}
|
|
|
|
/**
|
|
* Builds a graph from a list of nodes.
|
|
* All segments in each node group will be connected with all the segments of
|
|
* the next group and so on.
|
|
*
|
|
* At each connection will be assigned a weight depending on the
|
|
* segment's byte length.
|
|
*
|
|
* @param {Array} nodes Array of object with segments data
|
|
* @param {Number} version QR Code version
|
|
* @return {Object} Graph of all possible segments
|
|
*/
|
|
function buildGraph (nodes, version) {
|
|
const table = {}
|
|
const graph = { start: {} }
|
|
let prevNodeIds = ['start']
|
|
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const nodeGroup = nodes[i]
|
|
const currentNodeIds = []
|
|
|
|
for (let j = 0; j < nodeGroup.length; j++) {
|
|
const node = nodeGroup[j]
|
|
const key = '' + i + j
|
|
|
|
currentNodeIds.push(key)
|
|
table[key] = { node: node, lastCount: 0 }
|
|
graph[key] = {}
|
|
|
|
for (let n = 0; n < prevNodeIds.length; n++) {
|
|
const prevNodeId = prevNodeIds[n]
|
|
|
|
if (table[prevNodeId] && table[prevNodeId].node.mode === node.mode) {
|
|
graph[prevNodeId][key] =
|
|
getSegmentBitsLength(table[prevNodeId].lastCount + node.length, node.mode) -
|
|
getSegmentBitsLength(table[prevNodeId].lastCount, node.mode)
|
|
|
|
table[prevNodeId].lastCount += node.length
|
|
} else {
|
|
if (table[prevNodeId]) table[prevNodeId].lastCount = node.length
|
|
|
|
graph[prevNodeId][key] = getSegmentBitsLength(node.length, node.mode) +
|
|
4 + Mode.getCharCountIndicator(node.mode, version) // switch cost
|
|
}
|
|
}
|
|
}
|
|
|
|
prevNodeIds = currentNodeIds
|
|
}
|
|
|
|
for (let n = 0; n < prevNodeIds.length; n++) {
|
|
graph[prevNodeIds[n]].end = 0
|
|
}
|
|
|
|
return { map: graph, table: table }
|
|
}
|
|
|
|
/**
|
|
* Builds a segment from a specified data and mode.
|
|
* If a mode is not specified, the more suitable will be used.
|
|
*
|
|
* @param {String} data Input data
|
|
* @param {Mode | String} modesHint Data mode
|
|
* @return {Segment} Segment
|
|
*/
|
|
function buildSingleSegment (data, modesHint) {
|
|
let mode
|
|
const bestMode = Mode.getBestModeForData(data)
|
|
|
|
mode = Mode.from(modesHint, bestMode)
|
|
|
|
// Make sure data can be encoded
|
|
if (mode !== Mode.BYTE && mode.bit < bestMode.bit) {
|
|
throw new Error('"' + data + '"' +
|
|
' cannot be encoded with mode ' + Mode.toString(mode) +
|
|
'.\n Suggested mode is: ' + Mode.toString(bestMode))
|
|
}
|
|
|
|
// Use Mode.BYTE if Kanji support is disabled
|
|
if (mode === Mode.KANJI && !Utils.isKanjiModeEnabled()) {
|
|
mode = Mode.BYTE
|
|
}
|
|
|
|
switch (mode) {
|
|
case Mode.NUMERIC:
|
|
return new NumericData(data)
|
|
|
|
case Mode.ALPHANUMERIC:
|
|
return new AlphanumericData(data)
|
|
|
|
case Mode.KANJI:
|
|
return new KanjiData(data)
|
|
|
|
case Mode.BYTE:
|
|
return new ByteData(data)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds a list of segments from an array.
|
|
* Array can contain Strings or Objects with segment's info.
|
|
*
|
|
* For each item which is a string, will be generated a segment with the given
|
|
* string and the more appropriate encoding mode.
|
|
*
|
|
* For each item which is an object, will be generated a segment with the given
|
|
* data and mode.
|
|
* Objects must contain at least the property "data".
|
|
* If property "mode" is not present, the more suitable mode will be used.
|
|
*
|
|
* @param {Array} array Array of objects with segments data
|
|
* @return {Array} Array of Segments
|
|
*/
|
|
exports.fromArray = function fromArray (array) {
|
|
return array.reduce(function (acc, seg) {
|
|
if (typeof seg === 'string') {
|
|
acc.push(buildSingleSegment(seg, null))
|
|
} else if (seg.data) {
|
|
acc.push(buildSingleSegment(seg.data, seg.mode))
|
|
}
|
|
|
|
return acc
|
|
}, [])
|
|
}
|
|
|
|
/**
|
|
* Builds an optimized sequence of segments from a string,
|
|
* which will produce the shortest possible bitstream.
|
|
*
|
|
* @param {String} data Input string
|
|
* @param {Number} version QR Code version
|
|
* @return {Array} Array of segments
|
|
*/
|
|
exports.fromString = function fromString (data, version) {
|
|
const segs = getSegmentsFromString(data, Utils.isKanjiModeEnabled())
|
|
|
|
const nodes = buildNodes(segs)
|
|
const graph = buildGraph(nodes, version)
|
|
const path = dijkstra.find_path(graph.map, 'start', 'end')
|
|
|
|
const optimizedSegs = []
|
|
for (let i = 1; i < path.length - 1; i++) {
|
|
optimizedSegs.push(graph.table[path[i]].node)
|
|
}
|
|
|
|
return exports.fromArray(mergeSegments(optimizedSegs))
|
|
}
|
|
|
|
/**
|
|
* Splits a string in various segments with the modes which
|
|
* best represent their content.
|
|
* The produced segments are far from being optimized.
|
|
* The output of this function is only used to estimate a QR Code version
|
|
* which may contain the data.
|
|
*
|
|
* @param {string} data Input string
|
|
* @return {Array} Array of segments
|
|
*/
|
|
exports.rawSplit = function rawSplit (data) {
|
|
return exports.fromArray(
|
|
getSegmentsFromString(data, Utils.isKanjiModeEnabled())
|
|
)
|
|
}
|