From 4c83ab621bdc69f4a1dcb950005db5a1497c100b Mon Sep 17 00:00:00 2001 From: Tphamtranba <119475427+Bapham12@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:22:43 +0700 Subject: [PATCH] =?UTF-8?q?Th=C3=AAm=20TabularImportExport=20v=C3=A0o=20Ga?= =?UTF-8?q?dget=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Other/PapaParse.js | 689 ++++++++++++++++++ .../Gadget-TabularImportExport.js | 516 +++++++++++++ 2 files changed, 1205 insertions(+) create mode 100644 Other/PapaParse.js create mode 100644 TabularImportExport/Gadget-TabularImportExport.js diff --git a/Other/PapaParse.js b/Other/PapaParse.js new file mode 100644 index 0000000..90f9639 --- /dev/null +++ b/Other/PapaParse.js @@ -0,0 +1,689 @@ +/* @license +Papa Parse +v5.4.1 +https://github.com/mholt/PapaParse +License: MIT +*/ +! function(e, t) { + "function" == typeof define && define.amd ? define([], t) : "object" == typeof module && "undefined" != typeof exports ? module.exports = t() : e.Papa = t() +}(this, function s() { + "use strict"; + var f = "undefined" != typeof self ? self : "undefined" != typeof window ? window : void 0 !== f ? f : {}; + var n = !f.document && !!f.postMessage, + o = f.IS_PAPA_WORKER || !1, + a = {}, + u = 0, + b = { + parse: function(e, t) { + var r = (t = t || {}).dynamicTyping || !1; + J(r) && (t.dynamicTypingFunction = r, r = {}); + if (t.dynamicTyping = r, t.transform = !!J(t.transform) && t.transform, t.worker && b.WORKERS_SUPPORTED) { + var i = function() { + if (!b.WORKERS_SUPPORTED) return !1; + var e = (r = f.URL || f.webkitURL || null, i = s.toString(), b.BLOB_URL || (b.BLOB_URL = r.createObjectURL(new Blob(["var global = (function() { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } return {}; })(); global.IS_PAPA_WORKER=true; ", "(", i, ")();"], { + type: "text/javascript" + })))), + t = new f.Worker(e); + var r, i; + return t.onmessage = _, t.id = u++, a[t.id] = t + }(); + return i.userStep = t.step, i.userChunk = t.chunk, i.userComplete = t.complete, i.userError = t.error, t.step = J(t.step), t.chunk = J(t.chunk), t.complete = J(t.complete), t.error = J(t.error), delete t.worker, void i.postMessage({ + input: e, + config: t, + workerId: i.id + }) + } + var n = null; + b.NODE_STREAM_INPUT, "string" == typeof e ? (e = function(e) { + if (65279 === e.charCodeAt(0)) return e.slice(1); + return e + }(e), n = t.download ? new l(t) : new p(t)) : !0 === e.readable && J(e.read) && J(e.on) ? n = new g(t) : (f.File && e instanceof File || e instanceof Object) && (n = new c(t)); + return n.stream(e) + }, + unparse: function(e, t) { + var n = !1, + _ = !0, + m = ",", + y = "\r\n", + s = '"', + a = s + s, + r = !1, + i = null, + o = !1; + ! function() { + if ("object" != typeof t) return; + "string" != typeof t.delimiter || b.BAD_DELIMITERS.filter(function(e) { + return -1 !== t.delimiter.indexOf(e) + }).length || (m = t.delimiter); + ("boolean" == typeof t.quotes || "function" == typeof t.quotes || Array.isArray(t.quotes)) && (n = t.quotes); + "boolean" != typeof t.skipEmptyLines && "string" != typeof t.skipEmptyLines || (r = t.skipEmptyLines); + "string" == typeof t.newline && (y = t.newline); + "string" == typeof t.quoteChar && (s = t.quoteChar); + "boolean" == typeof t.header && (_ = t.header); + if (Array.isArray(t.columns)) { + if (0 === t.columns.length) throw new Error("Option columns is empty"); + i = t.columns + } + void 0 !== t.escapeChar && (a = t.escapeChar + s); + ("boolean" == typeof t.escapeFormulae || t.escapeFormulae instanceof RegExp) && (o = t.escapeFormulae instanceof RegExp ? t.escapeFormulae : /^[=+\-@\t\r].*$/) + }(); + var u = new RegExp(Q(s), "g"); + "string" == typeof e && (e = JSON.parse(e)); + if (Array.isArray(e)) { + if (!e.length || Array.isArray(e[0])) return h(null, e, r); + if ("object" == typeof e[0]) return h(i || Object.keys(e[0]), e, r) + } else if ("object" == typeof e) return "string" == typeof e.data && (e.data = JSON.parse(e.data)), Array.isArray(e.data) && (e.fields || (e.fields = e.meta && e.meta.fields || i), e.fields || (e.fields = Array.isArray(e.data[0]) ? e.fields : "object" == typeof e.data[0] ? Object.keys(e.data[0]) : []), Array.isArray(e.data[0]) || "object" == typeof e.data[0] || (e.data = [e.data])), h(e.fields || [], e.data || [], r); + throw new Error("Unable to serialize unrecognized input"); + + function h(e, t, r) { + var i = ""; + "string" == typeof e && (e = JSON.parse(e)), "string" == typeof t && (t = JSON.parse(t)); + var n = Array.isArray(e) && 0 < e.length, + s = !Array.isArray(t[0]); + if (n && _) { + for (var a = 0; a < e.length; a++) 0 < a && (i += m), i += v(e[a], a); + 0 < t.length && (i += y) + } + for (var o = 0; o < t.length; o++) { + var u = n ? e.length : t[o].length, + h = !1, + f = n ? 0 === Object.keys(t[o]).length : 0 === t[o].length; + if (r && !n && (h = "greedy" === r ? "" === t[o].join("").trim() : 1 === t[o].length && 0 === t[o][0].length), "greedy" === r && n) { + for (var d = [], l = 0; l < u; l++) { + var c = s ? e[l] : l; + d.push(t[o][c]) + } + h = "" === d.join("").trim() + } + if (!h) { + for (var p = 0; p < u; p++) { + 0 < p && !f && (i += m); + var g = n && s ? e[p] : p; + i += v(t[o][g], p) + } + o < t.length - 1 && (!r || 0 < u && !f) && (i += y) + } + } + return i + } + + function v(e, t) { + if (null == e) return ""; + if (e.constructor === Date) return JSON.stringify(e).slice(1, 25); + var r = !1; + o && "string" == typeof e && o.test(e) && (e = "'" + e, r = !0); + var i = e.toString().replace(u, a); + return (r = r || !0 === n || "function" == typeof n && n(e, t) || Array.isArray(n) && n[t] || function(e, t) { + for (var r = 0; r < t.length; r++) + if (-1 < e.indexOf(t[r])) return !0; + return !1 + }(i, b.BAD_DELIMITERS) || -1 < i.indexOf(m) || " " === i.charAt(0) || " " === i.charAt(i.length - 1)) ? s + i + s : i + } + } + }; + if (b.RECORD_SEP = String.fromCharCode(30), b.UNIT_SEP = String.fromCharCode(31), b.BYTE_ORDER_MARK = "\ufeff", b.BAD_DELIMITERS = ["\r", "\n", '"', b.BYTE_ORDER_MARK], b.WORKERS_SUPPORTED = !n && !!f.Worker, b.NODE_STREAM_INPUT = 1, b.LocalChunkSize = 10485760, b.RemoteChunkSize = 5242880, b.DefaultDelimiter = ",", b.Parser = E, b.ParserHandle = r, b.NetworkStreamer = l, b.FileStreamer = c, b.StringStreamer = p, b.ReadableStreamStreamer = g, f.jQuery) { + var d = f.jQuery; + d.fn.parse = function(o) { + var r = o.config || {}, + u = []; + return this.each(function(e) { + if (!("INPUT" === d(this).prop("tagName").toUpperCase() && "file" === d(this).attr("type").toLowerCase() && f.FileReader) || !this.files || 0 === this.files.length) return !0; + for (var t = 0; t < this.files.length; t++) u.push({ + file: this.files[t], + inputElem: this, + instanceConfig: d.extend({}, r) + }) + }), e(), this; + + function e() { + if (0 !== u.length) { + var e, t, r, i, n = u[0]; + if (J(o.before)) { + var s = o.before(n.file, n.inputElem); + if ("object" == typeof s) { + if ("abort" === s.action) return e = "AbortError", t = n.file, r = n.inputElem, i = s.reason, void(J(o.error) && o.error({ + name: e + }, t, r, i)); + if ("skip" === s.action) return void h(); + "object" == typeof s.config && (n.instanceConfig = d.extend(n.instanceConfig, s.config)) + } else if ("skip" === s) return void h() + } + var a = n.instanceConfig.complete; + n.instanceConfig.complete = function(e) { + J(a) && a(e, n.file, n.inputElem), h() + }, b.parse(n.file, n.instanceConfig) + } else J(o.complete) && o.complete() + } + + function h() { + u.splice(0, 1), e() + } + } + } + + function h(e) { + this._handle = null, this._finished = !1, this._completed = !1, this._halted = !1, this._input = null, this._baseIndex = 0, this._partialLine = "", this._rowCount = 0, this._start = 0, this._nextChunk = null, this.isFirstChunk = !0, this._completeResults = { + data: [], + errors: [], + meta: {} + }, + function(e) { + var t = w(e); + t.chunkSize = parseInt(t.chunkSize), e.step || e.chunk || (t.chunkSize = null); + this._handle = new r(t), (this._handle.streamer = this)._config = t + }.call(this, e), this.parseChunk = function(e, t) { + if (this.isFirstChunk && J(this._config.beforeFirstChunk)) { + var r = this._config.beforeFirstChunk(e); + void 0 !== r && (e = r) + } + this.isFirstChunk = !1, this._halted = !1; + var i = this._partialLine + e; + this._partialLine = ""; + var n = this._handle.parse(i, this._baseIndex, !this._finished); + if (!this._handle.paused() && !this._handle.aborted()) { + var s = n.meta.cursor; + this._finished || (this._partialLine = i.substring(s - this._baseIndex), this._baseIndex = s), n && n.data && (this._rowCount += n.data.length); + var a = this._finished || this._config.preview && this._rowCount >= this._config.preview; + if (o) f.postMessage({ + results: n, + workerId: b.WORKER_ID, + finished: a + }); + else if (J(this._config.chunk) && !t) { + if (this._config.chunk(n, this._handle), this._handle.paused() || this._handle.aborted()) return void(this._halted = !0); + n = void 0, this._completeResults = void 0 + } + return this._config.step || this._config.chunk || (this._completeResults.data = this._completeResults.data.concat(n.data), this._completeResults.errors = this._completeResults.errors.concat(n.errors), this._completeResults.meta = n.meta), this._completed || !a || !J(this._config.complete) || n && n.meta.aborted || (this._config.complete(this._completeResults, this._input), this._completed = !0), a || n && n.meta.paused || this._nextChunk(), n + } + this._halted = !0 + }, this._sendError = function(e) { + J(this._config.error) ? this._config.error(e) : o && this._config.error && f.postMessage({ + workerId: b.WORKER_ID, + error: e, + finished: !1 + }) + } + } + + function l(e) { + var i; + (e = e || {}).chunkSize || (e.chunkSize = b.RemoteChunkSize), h.call(this, e), this._nextChunk = n ? function() { + this._readChunk(), this._chunkLoaded() + } : function() { + this._readChunk() + }, this.stream = function(e) { + this._input = e, this._nextChunk() + }, this._readChunk = function() { + if (this._finished) this._chunkLoaded(); + else { + if (i = new XMLHttpRequest, this._config.withCredentials && (i.withCredentials = this._config.withCredentials), n || (i.onload = v(this._chunkLoaded, this), i.onerror = v(this._chunkError, this)), i.open(this._config.downloadRequestBody ? "POST" : "GET", this._input, !n), this._config.downloadRequestHeaders) { + var e = this._config.downloadRequestHeaders; + for (var t in e) i.setRequestHeader(t, e[t]) + } + if (this._config.chunkSize) { + var r = this._start + this._config.chunkSize - 1; + i.setRequestHeader("Range", "bytes=" + this._start + "-" + r) + } + try { + i.send(this._config.downloadRequestBody) + } catch (e) { + this._chunkError(e.message) + } + n && 0 === i.status && this._chunkError() + } + }, this._chunkLoaded = function() { + 4 === i.readyState && (i.status < 200 || 400 <= i.status ? this._chunkError() : (this._start += this._config.chunkSize ? this._config.chunkSize : i.responseText.length, this._finished = !this._config.chunkSize || this._start >= function(e) { + var t = e.getResponseHeader("Content-Range"); + if (null === t) return -1; + return parseInt(t.substring(t.lastIndexOf("/") + 1)) + }(i), this.parseChunk(i.responseText))) + }, this._chunkError = function(e) { + var t = i.statusText || e; + this._sendError(new Error(t)) + } + } + + function c(e) { + var i, n; + (e = e || {}).chunkSize || (e.chunkSize = b.LocalChunkSize), h.call(this, e); + var s = "undefined" != typeof FileReader; + this.stream = function(e) { + this._input = e, n = e.slice || e.webkitSlice || e.mozSlice, s ? ((i = new FileReader).onload = v(this._chunkLoaded, this), i.onerror = v(this._chunkError, this)) : i = new FileReaderSync, this._nextChunk() + }, this._nextChunk = function() { + this._finished || this._config.preview && !(this._rowCount < this._config.preview) || this._readChunk() + }, this._readChunk = function() { + var e = this._input; + if (this._config.chunkSize) { + var t = Math.min(this._start + this._config.chunkSize, this._input.size); + e = n.call(e, this._start, t) + } + var r = i.readAsText(e, this._config.encoding); + s || this._chunkLoaded({ + target: { + result: r + } + }) + }, this._chunkLoaded = function(e) { + this._start += this._config.chunkSize, this._finished = !this._config.chunkSize || this._start >= this._input.size, this.parseChunk(e.target.result) + }, this._chunkError = function() { + this._sendError(i.error) + } + } + + function p(e) { + var r; + h.call(this, e = e || {}), this.stream = function(e) { + return r = e, this._nextChunk() + }, this._nextChunk = function() { + if (!this._finished) { + var e, t = this._config.chunkSize; + return t ? (e = r.substring(0, t), r = r.substring(t)) : (e = r, r = ""), this._finished = !r, this.parseChunk(e) + } + } + } + + function g(e) { + h.call(this, e = e || {}); + var t = [], + r = !0, + i = !1; + this.pause = function() { + h.prototype.pause.apply(this, arguments), this._input.pause() + }, this.resume = function() { + h.prototype.resume.apply(this, arguments), this._input.resume() + }, this.stream = function(e) { + this._input = e, this._input.on("data", this._streamData), this._input.on("end", this._streamEnd), this._input.on("error", this._streamError) + }, this._checkIsFinished = function() { + i && 1 === t.length && (this._finished = !0) + }, this._nextChunk = function() { + this._checkIsFinished(), t.length ? this.parseChunk(t.shift()) : r = !0 + }, this._streamData = v(function(e) { + try { + t.push("string" == typeof e ? e : e.toString(this._config.encoding)), r && (r = !1, this._checkIsFinished(), this.parseChunk(t.shift())) + } catch (e) { + this._streamError(e) + } + }, this), this._streamError = v(function(e) { + this._streamCleanUp(), this._sendError(e) + }, this), this._streamEnd = v(function() { + this._streamCleanUp(), i = !0, this._streamData("") + }, this), this._streamCleanUp = v(function() { + this._input.removeListener("data", this._streamData), this._input.removeListener("end", this._streamEnd), this._input.removeListener("error", this._streamError) + }, this) + } + + function r(m) { + var a, o, u, i = Math.pow(2, 53), + n = -i, + s = /^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/, + h = /^((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)))$/, + t = this, + r = 0, + f = 0, + d = !1, + e = !1, + l = [], + c = { + data: [], + errors: [], + meta: {} + }; + if (J(m.step)) { + var p = m.step; + m.step = function(e) { + if (c = e, _()) g(); + else { + if (g(), 0 === c.data.length) return; + r += e.data.length, m.preview && r > m.preview ? o.abort() : (c.data = c.data[0], p(c, t)) + } + } + } + + function y(e) { + return "greedy" === m.skipEmptyLines ? "" === e.join("").trim() : 1 === e.length && 0 === e[0].length + } + + function g() { + return c && u && (k("Delimiter", "UndetectableDelimiter", "Unable to auto-detect delimiting character; defaulted to '" + b.DefaultDelimiter + "'"), u = !1), m.skipEmptyLines && (c.data = c.data.filter(function(e) { + return !y(e) + })), _() && function() { + if (!c) return; + + function e(e, t) { + J(m.transformHeader) && (e = m.transformHeader(e, t)), l.push(e) + } + if (Array.isArray(c.data[0])) { + for (var t = 0; _() && t < c.data.length; t++) c.data[t].forEach(e); + c.data.splice(0, 1) + } else c.data.forEach(e) + }(), + function() { + if (!c || !m.header && !m.dynamicTyping && !m.transform) return c; + + function e(e, t) { + var r, i = m.header ? {} : []; + for (r = 0; r < e.length; r++) { + var n = r, + s = e[r]; + m.header && (n = r >= l.length ? "__parsed_extra" : l[r]), m.transform && (s = m.transform(s, n)), s = v(n, s), "__parsed_extra" === n ? (i[n] = i[n] || [], i[n].push(s)) : i[n] = s + } + return m.header && (r > l.length ? k("FieldMismatch", "TooManyFields", "Too many fields: expected " + l.length + " fields but parsed " + r, f + t) : r < l.length && k("FieldMismatch", "TooFewFields", "Too few fields: expected " + l.length + " fields but parsed " + r, f + t)), i + } + var t = 1; + !c.data.length || Array.isArray(c.data[0]) ? (c.data = c.data.map(e), t = c.data.length) : c.data = e(c.data, 0); + m.header && c.meta && (c.meta.fields = l); + return f += t, c + }() + } + + function _() { + return m.header && 0 === l.length + } + + function v(e, t) { + return r = e, m.dynamicTypingFunction && void 0 === m.dynamicTyping[r] && (m.dynamicTyping[r] = m.dynamicTypingFunction(r)), !0 === (m.dynamicTyping[r] || m.dynamicTyping) ? "true" === t || "TRUE" === t || "false" !== t && "FALSE" !== t && (function(e) { + if (s.test(e)) { + var t = parseFloat(e); + if (n < t && t < i) return !0 + } + return !1 + }(t) ? parseFloat(t) : h.test(t) ? new Date(t) : "" === t ? null : t) : t; + var r + } + + function k(e, t, r, i) { + var n = { + type: e, + code: t, + message: r + }; + void 0 !== i && (n.row = i), c.errors.push(n) + } + this.parse = function(e, t, r) { + var i = m.quoteChar || '"'; + if (m.newline || (m.newline = function(e, t) { + e = e.substring(0, 1048576); + var r = new RegExp(Q(t) + "([^]*?)" + Q(t), "gm"), + i = (e = e.replace(r, "")).split("\r"), + n = e.split("\n"), + s = 1 < n.length && n[0].length < i[0].length; + if (1 === i.length || s) return "\n"; + for (var a = 0, o = 0; o < i.length; o++) "\n" === i[o][0] && a++; + return a >= i.length / 2 ? "\r\n" : "\r" + }(e, i)), u = !1, m.delimiter) J(m.delimiter) && (m.delimiter = m.delimiter(e), c.meta.delimiter = m.delimiter); + else { + var n = function(e, t, r, i, n) { + var s, a, o, u; + n = n || [",", "\t", "|", ";", b.RECORD_SEP, b.UNIT_SEP]; + for (var h = 0; h < n.length; h++) { + var f = n[h], + d = 0, + l = 0, + c = 0; + o = void 0; + for (var p = new E({ + comments: i, + delimiter: f, + newline: t, + preview: 10 + }).parse(e), g = 0; g < p.data.length; g++) + if (r && y(p.data[g])) c++; + else { + var _ = p.data[g].length; + l += _, void 0 !== o ? 0 < _ && (d += Math.abs(_ - o), o = _) : o = _ + } 0 < p.data.length && (l /= p.data.length - c), (void 0 === a || d <= a) && (void 0 === u || u < l) && 1.99 < l && (a = d, s = f, u = l) + } + return { + successful: !!(m.delimiter = s), + bestDelimiter: s + } + }(e, m.newline, m.skipEmptyLines, m.comments, m.delimitersToGuess); + n.successful ? m.delimiter = n.bestDelimiter : (u = !0, m.delimiter = b.DefaultDelimiter), c.meta.delimiter = m.delimiter + } + var s = w(m); + return m.preview && m.header && s.preview++, a = e, o = new E(s), c = o.parse(a, t, r), g(), d ? { + meta: { + paused: !0 + } + } : c || { + meta: { + paused: !1 + } + } + }, this.paused = function() { + return d + }, this.pause = function() { + d = !0, o.abort(), a = J(m.chunk) ? "" : a.substring(o.getCharIndex()) + }, this.resume = function() { + t.streamer._halted ? (d = !1, t.streamer.parseChunk(a, !0)) : setTimeout(t.resume, 3) + }, this.aborted = function() { + return e + }, this.abort = function() { + e = !0, o.abort(), c.meta.aborted = !0, J(m.complete) && m.complete(c), a = "" + } + } + + function Q(e) { + return e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + } + + function E(j) { + var z, M = (j = j || {}).delimiter, + P = j.newline, + U = j.comments, + q = j.step, + N = j.preview, + B = j.fastMode, + K = z = void 0 === j.quoteChar || null === j.quoteChar ? '"' : j.quoteChar; + if (void 0 !== j.escapeChar && (K = j.escapeChar), ("string" != typeof M || -1 < b.BAD_DELIMITERS.indexOf(M)) && (M = ","), U === M) throw new Error("Comment character same as delimiter"); + !0 === U ? U = "#" : ("string" != typeof U || -1 < b.BAD_DELIMITERS.indexOf(U)) && (U = !1), "\n" !== P && "\r" !== P && "\r\n" !== P && (P = "\n"); + var W = 0, + H = !1; + this.parse = function(i, t, r) { + if ("string" != typeof i) throw new Error("Input must be a string"); + var n = i.length, + e = M.length, + s = P.length, + a = U.length, + o = J(q), + u = [], + h = [], + f = [], + d = W = 0; + if (!i) return L(); + if (j.header && !t) { + var l = i.split(P)[0].split(M), + c = [], + p = {}, + g = !1; + for (var _ in l) { + var m = l[_]; + J(j.transformHeader) && (m = j.transformHeader(m, _)); + var y = m, + v = p[m] || 0; + for (0 < v && (g = !0, y = m + "_" + v), p[m] = v + 1; c.includes(y);) y = y + "_" + v; + c.push(y) + } + if (g) { + var k = i.split(P); + k[0] = c.join(M), i = k.join(P) + } + } + if (B || !1 !== B && -1 === i.indexOf(z)) { + for (var b = i.split(P), E = 0; E < b.length; E++) { + if (f = b[E], W += f.length, E !== b.length - 1) W += P.length; + else if (r) return L(); + if (!U || f.substring(0, a) !== U) { + if (o) { + if (u = [], I(f.split(M)), F(), H) return L() + } else I(f.split(M)); + if (N && N <= E) return u = u.slice(0, N), L(!0) + } + } + return L() + } + for (var w = i.indexOf(M, W), R = i.indexOf(P, W), C = new RegExp(Q(K) + Q(z), "g"), S = i.indexOf(z, W);;) + if (i[W] !== z) + if (U && 0 === f.length && i.substring(W, W + a) === U) { + if (-1 === R) return L(); + W = R + s, R = i.indexOf(P, W), w = i.indexOf(M, W) + } else if (-1 !== w && (w < R || -1 === R)) f.push(i.substring(W, w)), W = w + e, w = i.indexOf(M, W); + else { + if (-1 === R) break; + if (f.push(i.substring(W, R)), D(R + s), o && (F(), H)) return L(); + if (N && u.length >= N) return L(!0) + } else + for (S = W, W++;;) { + if (-1 === (S = i.indexOf(z, S + 1))) return r || h.push({ + type: "Quotes", + code: "MissingQuotes", + message: "Quoted field unterminated", + row: u.length, + index: W + }), T(); + if (S === n - 1) return T(i.substring(W, S).replace(C, z)); + if (z !== K || i[S + 1] !== K) { + if (z === K || 0 === S || i[S - 1] !== K) { + -1 !== w && w < S + 1 && (w = i.indexOf(M, S + 1)), -1 !== R && R < S + 1 && (R = i.indexOf(P, S + 1)); + var O = A(-1 === R ? w : Math.min(w, R)); + if (i.substr(S + 1 + O, e) === M) { + f.push(i.substring(W, S).replace(C, z)), i[W = S + 1 + O + e] !== z && (S = i.indexOf(z, W)), w = i.indexOf(M, W), R = i.indexOf(P, W); + break + } + var x = A(R); + if (i.substring(S + 1 + x, S + 1 + x + s) === P) { + if (f.push(i.substring(W, S).replace(C, z)), D(S + 1 + x + s), w = i.indexOf(M, W), S = i.indexOf(z, W), o && (F(), H)) return L(); + if (N && u.length >= N) return L(!0); + break + } + h.push({ + type: "Quotes", + code: "InvalidQuotes", + message: "Trailing quote on quoted field is malformed", + row: u.length, + index: W + }), S++ + } + } else S++ + } + return T(); + + function I(e) { + u.push(e), d = W + } + + function A(e) { + var t = 0; + if (-1 !== e) { + var r = i.substring(S + 1, e); + r && "" === r.trim() && (t = r.length) + } + return t + } + + function T(e) { + return r || (void 0 === e && (e = i.substring(W)), f.push(e), W = n, I(f), o && F()), L() + } + + function D(e) { + W = e, I(f), f = [], R = i.indexOf(P, W) + } + + function L(e) { + return { + data: u, + errors: h, + meta: { + delimiter: M, + linebreak: P, + aborted: H, + truncated: !!e, + cursor: d + (t || 0) + } + } + } + + function F() { + q(L()), u = [], h = [] + } + }, this.abort = function() { + H = !0 + }, this.getCharIndex = function() { + return W + } + } + + function _(e) { + var t = e.data, + r = a[t.workerId], + i = !1; + if (t.error) r.userError(t.error, t.file); + else if (t.results && t.results.data) { + var n = { + abort: function() { + i = !0, m(t.workerId, { + data: [], + errors: [], + meta: { + aborted: !0 + } + }) + }, + pause: y, + resume: y + }; + if (J(r.userStep)) { + for (var s = 0; s < t.results.data.length && (r.userStep({ + data: t.results.data[s], + errors: t.results.errors, + meta: t.results.meta + }, n), !i); s++); + delete t.results + } else J(r.userChunk) && (r.userChunk(t.results, n, t.file), delete t.results) + } + t.finished && !i && m(t.workerId, t.results) + } + + function m(e, t) { + var r = a[e]; + J(r.userComplete) && r.userComplete(t), r.terminate(), delete a[e] + } + + function y() { + throw new Error("Not implemented.") + } + + function w(e) { + if ("object" != typeof e || null === e) return e; + var t = Array.isArray(e) ? [] : {}; + for (var r in e) t[r] = w(e[r]); + return t + } + + function v(e, t) { + return function() { + e.apply(t, arguments) + } + } + + function J(e) { + return "function" == typeof e + } + return o && (f.onmessage = function(e) { + var t = e.data; + void 0 === b.WORKER_ID && t && (b.WORKER_ID = t.workerId); + if ("string" == typeof t.input) f.postMessage({ + workerId: b.WORKER_ID, + results: b.parse(t.input, t.config), + finished: !0 + }); + else if (f.File && t.input instanceof File || t.input instanceof Object) { + var r = b.parse(t.input, t.config); + r && f.postMessage({ + workerId: b.WORKER_ID, + results: r, + finished: !0 + }) + } + }), (l.prototype = Object.create(h.prototype)).constructor = l, (c.prototype = Object.create(h.prototype)).constructor = c, (p.prototype = Object.create(p.prototype)).constructor = p, (g.prototype = Object.create(h.prototype)).constructor = g, b +}); \ No newline at end of file diff --git a/TabularImportExport/Gadget-TabularImportExport.js b/TabularImportExport/Gadget-TabularImportExport.js new file mode 100644 index 0000000..6ca2541 --- /dev/null +++ b/TabularImportExport/Gadget-TabularImportExport.js @@ -0,0 +1,516 @@ +/** + * Copyright (c) 2017-2025 Derk-Jan Hartman [[c:User:TheDJ]] + * + * 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. + */ +/** + * This script provides import and export functionality of CSV and XLSX files + * into Commons' tabular data sets. + * + * Known issues: + * - You always need to make sure your file has a header + * - Types of imported data are a bit of guess work + * - The localized type gets exported as the first entry for the csv/tsv + * + * @param $ + * @param mw + */ +( function ( $, mw ) { + + const papaScript = mw.loader.getScript( 'https://blutigeskareuz.miraheze.org/w/index.php?title=MediaWiki:PapaParse.js&action=raw&ctype=text/javascript' ); + const xlsxScript = mw.loader.getScript( 'https://cdn.jsdelivr.net/npm/js-xlsx@0.8.22/dist/xlsx.full.min.js' ); + + const deepCopy = ( object ) => $.extend( true, {}, object ); + const deepCopyArray = ( array ) => $.extend( true, [], array ); + + const addKeysToSet = ( set, obj ) => { + Object.keys( obj ).forEach( ( key ) => set.add( key ) ); + }; + + const isNonNullObject = ( value ) => typeof value === 'object' && value !== null; + + function saveAs( blob, filename ) { + const url = URL.createObjectURL( blob ); + const a = document.createElement( 'a' ); + a.style.display = 'none'; + a.href = url; + a.download = filename || 'download'; + document.body.appendChild( a ); + + // this makes this function only work when the wrapping code executes immediately + a.click(); + setTimeout( () => { + document.body.removeChild( a ); + URL.revokeObjectURL( url ); + }, 1000 ); + } + + function Workbook() { + if ( !( this instanceof Workbook ) ) { + return new Workbook(); + } + this.SheetNames = []; + this.Sheets = {}; + } + + const i18n = { + 'gadget-tabular-import-placeholder': 'A .csv or .xlsx file to import', + 'gadget-tabular-export-csv': 'Export to CSV', + 'gadget-tabular-export-excel': 'Export to Excel', + 'gadget-tabular-unsupported-file': 'Unsupported file type. Please select either a .csv or .xlsx file.', + 'gadget-tabular-import-failed': 'Unable to import file: $1', + 'gadget-tabular-description-placeholder': 'Please enter a description', + 'gadget-tabular-importing-summary': 'Importing data $1', + 'gadget-tabular-imported-from': 'Imported from file $1 $2' + }; + + class TabularImportExport { + constructor() { + this.selectFileWidget = null; + this.api = null; + } + + install() { + this.api = new mw.Api(); + + if ( [ 'edit', 'submit' ].includes( mw.config.get( 'wgAction' ) ) ) { + this.selectFileWidget = new OO.ui.SelectFileInputWidget( { placeholder: mw.msg( 'gadget-tabular-import-placeholder' ) } ); + this.selectFileWidget.on( 'change', this.selectFile.bind( this ) ); + $( '#editform:not([readonly])' ).before( this.selectFileWidget.$element ); + } else if ( mw.config.get( 'wgAction' ) === 'view' ) { + const button1 = new OO.ui.ButtonWidget( { label: mw.msg( 'gadget-tabular-export-csv' ) } ); + button1.on( 'click', () => this.fetchData( 'csv' ) ); + const button2 = new OO.ui.ButtonWidget( { label: mw.msg( 'gadget-tabular-export-excel' ) } ); + button2.on( 'click', () => this.fetchData( 'excel' ) ); + + const layout = new OO.ui.HorizontalLayout( { + items: [ + button1, + button2 + ] + } ); + $( '#mw-content-text' ).append( layout.$element ); + + const skin = mw.config.get( 'skin' ); + let portletId = 'p-tb'; + if ( skin === 'monobook' ) { + portletId = 'p-cactions'; + } else if ( skin === 'vector' || skin === 'vector-2022' ) { + portletId = 'p-electronpdfservice-sidebar-portlet-heading'; + } + + $( mw.util.addPortletLink( portletId, '', mw.msg( 'gadget-tabular-export-csv', 't-tiec' ) ) ).on( 'click', ( e ) => { + this.fetchData( 'csv' ); + e.preventDefault(); + } ); + $( mw.util.addPortletLink( portletId, '', mw.msg( 'gadget-tabular-export-excel', 't-tiee' ) ) ).on( 'click', ( e ) => { + this.fetchData( 'excel' ); + e.preventDefault(); + } ); + } + } + + fetchData( type ) { + const apiRequest = this.api.get( { + action: 'query', + formatversion: 2, + format: 'json', + prop: 'revisions', + rvprop: 'ids|user|content|timestamp', + rvslots: 'main', + titles: mw.config.get( 'wgPageName' ) + } ); + + $.when( apiRequest, papaScript, xlsxScript ).then( ( apiResponse, papaResult, xlsxResult ) => { + let jsondata; + const query = apiResponse[ 0 ] && apiResponse[ 0 ].query; + const page = query && query.pages && query.pages[ 0 ]; + const rev = page && page.revisions && page.revisions[ 0 ]; + + if ( rev && rev.slots && rev.slots.main && rev.slots.main.content ) { + try { + jsondata = JSON.parse( rev.slots.main.content ); + } catch ( e ) { + OO.ui.alert( 'Could not parse tabular data: ' + e ); + return; + } + } + + if ( !jsondata ) { + OO.ui.alert( 'Could not find tabular data' ); + return; + } + + if ( type === 'csv' ) { + this.exportToCSV( jsondata, page ); + } else if ( type === 'excel' ) { + this.exportToExcel( jsondata, page ); + } + } ); + } + + exportToExcel( jsondata, pageData ) { + const workbook = this.convertToExcel( jsondata, pageData ); + const wopts = { bookType: 'xlsx', bookSST: false, type: 'array', charset:'utf-8', cellDates:true, cellStyles:true }; + XLSX.writeFile( workbook, `${ mw.config.get( 'wgTitle' ) }.xlsx`, wopts ); + } + + exportToCSV( jsondata, pageData ) { + const csvdata = { fields: [], data: [] }; + jsondata.schema.fields.forEach( ( element ) => { + csvdata.fields.push( element.name ); + } ); + jsondata.data.forEach( ( row ) => { + const rowArray = []; + row.forEach( ( xElement, xIndex ) => { + if ( jsondata.schema.fields[ xIndex ].type === 'localized' && typeof xElement === 'object' ) { + rowArray.push( Object.values( xElement )[ 0 ] ); + } else { + rowArray.push( xElement ); + } + } ); + csvdata.data.push( rowArray ); + } ); + const csv = Papa.unparse( csvdata ); + const blob = new Blob( [ csv ], { type: 'text/csv' } ); + saveAs( blob, `${ mw.config.get( 'wgTitle' ) }.csv` ); + } + + convertToExcel( jsondata, pageData ) { + const wb = new Workbook(); + const dataArray = [ [] ]; + const localizedIndices = []; + const languageCodes = new Set(); + + jsondata.schema.fields.forEach( ( field, idx ) => { + // Push headers onto the output array + dataArray[ 0 ].push( field.name ); + + // Collect known language codes + if ( field.title && typeof field.title === 'object' ) { + addKeysToSet( languageCodes, field.title ); + } + + // Find indices of columns with type 'localized' + if ( field && field.type === 'localized' ) { + localizedIndices.push( idx ); + } + } ); + + // Modify the array to get one of the localized values + jsondata.data.forEach( ( row ) => { + const copyOfRow = deepCopyArray( row ); + localizedIndices.forEach( ( idx ) => { + if ( isNonNullObject( copyOfRow[ idx ] ) ) { + addKeysToSet( languageCodes, copyOfRow[ idx ] ); + copyOfRow[ idx ] = Object.values( copyOfRow[ idx ] )[ 0 ]; + } + } ); + dataArray.push( copyOfRow ); + } ); + + const ws = XLSX.utils.aoa_to_sheet( dataArray ); + const ws_name = mw.config.get( 'wgTitle' ) + .replace( /[/\\*'?[\]:]/g, ' ' ) + .toLowerCase() + .slice( 0, 31 ); + + ws[ '!merges' ] = []; + wb.SheetNames.push( ws_name ); + wb.Sheets[ ws_name ] = ws; + + const copyDataWithReferences = (dataArray, dataSheetName) => { + return dataArray.map((row, i) => { + return row.map( (cell, j) => { + const cellName = XLSX.utils.encode_cell({r:i, c:j}); + return {f: `'${dataSheetName}'!${cellName}` }; + }); + }); + }; + + // dataArray is overwritten here repeatedly, but it overrides the same cells + // we leave hidden comments to mark cells as type localized, for later. + function createTranslatedArrayOfArrays( dataArray, lang ) { + jsondata.schema.fields.forEach( ( field, idx ) => { + // Push headers onto the output array + const firstEntry = Object.entries(field.title)[0]; + const cellValue = field.title[ lang ] || firstEntry[ 1 ]; + const cellLanguage = field.title[ lang ] ? lang : firstEntry[ 0 ] || ''; + const cell = {v: cellValue, c: [ { a: 'localized', t: cellLanguage, hidden: true} ] }; + dataArray[ 0 ][ idx ] = cell; + } ); + + jsondata.data.forEach( ( row, rowIndex ) => { + localizedIndices.forEach( ( idx ) => { + if ( isNonNullObject( row[ idx ] ) ) { + const firstEntry = Object.entries(obj)[0]; + const cellValue = row[ idx ][ lang ] || firstEntry[ 1 ] || null; + const cellLanguage = row[ idx ][ lang ] ? lang : firstEntry[ 0 ] || ''; + const cell = {v: cellValue, c: [ { a: 'localized', t: cellLanguage, hidden: true } ] }; + cell.c.hidden = true; + dataArray[rowIndex + 1][ idx ] = cell; + } + } ); + } ); + return dataArray; + } + + // A copy that refers back to the original sheet name for cell values + const dataArrayWithReferences = copyDataWithReferences( dataArray, ws_name ); + + // Add copies of the data for each languageCode + [ ...languageCodes ].sort().forEach( ( lang ) => { + const ws_name = lang; + const ws = XLSX.utils.aoa_to_sheet( createTranslatedArrayOfArrays( dataArrayWithReferences, lang ) ); + + wb.SheetNames.push( ws_name ); + wb.Sheets[ ws_name ] = ws; + } ); + + // Generate links + const pageUrl = new URL( mw.config.get( 'wgArticlePath' ).replace( '$1', pageData.title ), window.location ); + const historyUrl = new URL( mw.config.get( 'wgScript' ), window.location ); + historyUrl.searchParams.set( 'title', pageData.title ); + historyUrl.searchParams.set( 'action', 'history' ); + + // Save metadata to excel properties + wb.Props = { + Title: pageData.title, + Company: mw.config.get( 'wgSiteName' ), + SheetNames: wb.SheetNames, + Worksheets: wb.SheetNames.length + }; + wb.Custprops = { + Revision: pageData.revisions[ 0 ].revid, + LastModified: pageData.revisions[ 0 ].timestamp, + Url: pageUrl.toString(), + Contributors: historyUrl.toString(), + Sources: jsondata.sources || '', + LicenseCode: jsondata.license, + Software: 'Tabular Import/Export-gadget' + }; + for ( const language in jsondata.description ) { + wb.Custprops[ `Description.${ language }` ] = jsondata.description[ language ]; + } + if ( jsondata.mediaWikiCategories && jsondata.mediaWikiCategories.length > 0 ) { + wb.Custprops.MediaWikiCategories = jsondata.mediaWikiCategories.map( ( cat ) => { + let str = 'Category:' + cat.name; + if ( cat.sort ) { + str += '|' + cat.sort; + } + return str; + } ).join( ', ' ); + } + return wb; + } + + selectFile() { + const selectedFile = this.selectFileWidget.getValue(); + const name = selectedFile.name; + + $.when( papaScript, xlsxScript ).then( ( papaResult, xlsxResult ) => { + if ( name.endsWith( '.csv' ) || name.endsWith( '.tsv' ) || name.endsWith('.txt') ) { + this.importCSVFile( selectedFile ); + } else if ( name.endsWith( '.xlsx' ) || name.endsWith( '.xlsb' ) || name.endsWith( '.xls' ) || name.endsWith( '.ods' ) ) { + selectedFile.arrayBuffer().then( ( buffer ) => { + const workbook = XLSX.read( buffer, { type: 'array', dense:true, charset: 'utf-8' } ); + this.importXLSX( workbook ); + } ); + } else { + OO.ui.alert( mw.msg( 'gadget-tabular-unsupported-file' ) ); + } + } ); + } + + importCSVFile( file ) { + Papa.parse( file, { + header: true, + dynamicTyping: true, + complete: this.importCSV.bind( this ), + error: this.importCSVFailed.bind( this ), + encoding: 'utf-8' + } ); + } + + importCSV( csvdata ) { + const jsondata = { + schema: { fields: [] }, + data: [] + }; + + csvdata.meta.fields.forEach( ( columnName ) => { + jsondata.schema.fields.push( { + name: columnName.replace( /\W/g, '' ), + type: 'string', + title: { + en: columnName + } + } ); + } ); + csvdata.data.forEach( ( row ) => { + const columnData = []; + csvdata.meta.fields.forEach( ( columnName ) => { + columnData.push( row[ columnName ] === '' ? null : row[ columnName ] ); + } ); + jsondata.data.push( columnData ); + } ); + this.writeTextbox( jsondata ); + } + + importCSVFailed( error ) { + OO.ui.alert( mw.msg( 'gadget-tabular-import-failed', error ) ); + } + + importXLSX( workbook ) { + const jsondata = { + schema: { fields: [] }, + data: [] + }; + + if ( workbook.Custprops ) { + // If the workbook has custom properties, we can use them to fill in some metadata + if ( workbook.Custprops.LicenseCode ) { + jsondata.license = workbook.Custprops.LicenseCode; + } + if ( workbook.Custprops.Sources ) { + jsondata.sources = workbook.Custprops.Sources; + } + // Extract language descriptions from keys starting with 'Description.' + Object.entries( workbook.Custprops ).forEach( ( [ key, value ] ) => { + if ( key.startsWith( 'Description.' ) ) { + const lang = key.slice( 'Description.'.length ); + if ( !jsondata.description ) { + jsondata.description = {}; + } + jsondata.description[ lang ] = value; + } + } ); + if ( workbook.Custprops.MediaWikiCategories ) { + jsondata.mediaWikiCategories = workbook.Custprops.MediaWikiCategories.split( ', ' ).map( ( catStr ) => { + // Remove "Category:" prefix and split by '|' + const [ name, sort ] = catStr.replace( /^Category:/, '' ).split( '|' ); + const category = { name }; + if ( sort ) { + category.sort = sort; + } + return category; + } ); + } + } + + /* We only look at the first sheet */ + const sheet = workbook.Sheets[ workbook.SheetNames[ 0 ] ]; + const jsonsheet = XLSX.utils.sheet_to_json( sheet, { raw: true, defval: null } ); + if ( jsonsheet.length < 2 ) { + return; + } + for ( const header in jsonsheet[ 0 ] ) { + jsondata.schema.fields.push( { + name: header.replace( /\W/g, '' ), + type: 'string', + title: { + en: header + } + } ); + } + + jsonsheet.forEach( ( row ) => { + const rowData = []; + for ( const header in jsonsheet[ 0 ] ) { + let cell = row[ header ]; + if ( cell === undefined || cell === '' ) { + cell = null; + } + rowData.push( cell ); + } + jsondata.data.push( rowData ); + } ); + + if (workbook.SheetNames.length > 1) { + workbook.SheetNames.forEach( (name, index) => { + if ( workbook.Sheets[name]['!type'] !== undefined ) return; + workbook.Sheets[name].forEach( (row, i) => { + row.forEach( (cell, j) => { + if( cell.c && cell.c[0] && cell.c[0].a === 'localized' && cell.c[0].t === name ) { + if ( i === 0 ) { + if (jsondata.schema.fields[j].title ) { + jsondata.schema.fields[j].title[name] = cell.v; + } + return; + } + if ( typeof jsondata.data[i+1][j] === 'object' ) { + jsondata.data[i+1][j][cell.c[0].t] = cell.v; + } else { + jsondata.data[i+1][j] = { name: cell.v }; + } + } + } ); + } ); + }); + } + this.writeTextbox( jsondata ); + } + + writeTextbox( jsondata ) { + const selectFileWidget = this.selectFileWidget; + const selectedFile = selectFileWidget && selectFileWidget.getValue ? selectFileWidget.getValue() : { name: '' }; + + // Guess the data type, based on the first row of data + const firstRow = jsondata.data[ 0 ] || []; + firstRow.forEach( ( columnEl, index ) => { + if ( typeof columnEl === 'number' ) { + jsondata.schema.fields[ index ].type = 'number'; + } else if ( typeof columnEl === 'boolean' ) { + jsondata.schema.fields[ index ].type = 'boolean'; + } + } ); + + // Write some metadata + const username = mw.config.get( 'wgUserName' ); + const formattedUsername = username ? + `by ${ mw.config.get( 'wgFormattedNamespaces' )[ mw.config.get( 'wgNamespaceIds' ).user ] }:${ username }` : + ''; + const metadata = { + license: 'CC0-1.0', + description: { + en: mw.msg( 'gadget-tabular-description-placeholder' ) + }, + sources: mw.msg( 'gadget-tabular-imported-from', selectedFile.name, formattedUsername ) + }; + + /* Merge it */ + const merged = $.extend( metadata, jsondata ); + $( '#wpTextbox1' ).textSelection( 'setContents', JSON.stringify( merged, null, '\t' ) ); + $( '#wpSummary' ).val( mw.msg( 'gadget-tabular-importing-summary', selectedFile.name ) ); + } + } + + if ( mw.config.get( 'wgNamespaceNumber' ) === 486 && mw.config.get( 'wgTitle' ).endsWith( '.tab' ) ) { + mw.messages.set( i18n ); + $.when( + mw.loader.using( [ + 'oojs-ui', 'oojs-ui-core', 'oojs-ui-widgets', 'mediawiki.api', 'jquery.textSelection' + ] ), + $.ready + ).then( () => { + const tabularImportExport = new TabularImportExport(); + tabularImportExport.install(); + } ); + } +}( jQuery, mediaWiki ) ); \ No newline at end of file