var encoding = require('encoding-japanese');
var iconv = require('iconv-lite');
/**
* Represents a RawViewer object.
* @constructor
* @param {object} options - Options for configuring the RawViewer.
* @param {jQuery} [options.container=$("#previewRawData")] - The container element for the RawViewer.
*/
const RawViewer = function(options) {
/**
* Options for configuring the RawViewer.
* @type {object}
* @property {jQuery} [container=$("#previewRawData")] - The container element for the RawViewer.
*/
this.options = options || {};
/**
* The container element for the RawViewer.
* @type {jQuery}
*/
this.container = this.options.container || $("#previewRawData");
/**
* The feather value for the RawViewer.
* @type {number}
*/
this.feather = 2;
// Initialize the RawViewer
this.init();
};
// RawViewer.prototype.loadPrism = function() {
// if (this.prismIsLoaded) return;
// $("head").append(`<link href="modules/prismjs/prism.css" rel="stylesheet" />`);
// $("head").append(`<script src="modules/prismjs/prism.js"></script>`);
// this.prismIsLoaded = true;
// }
/**
* Loads Prism CSS stylesheet if not already loaded, and initializes Prism library if not already initialized.
*/
RawViewer.prototype.loadPrism = function() {
if (!$("#prismStyle").length) {
$("head").append(`<link href="modules/prismjs/prism.css" rel="stylesheet" id="prismStyle" />`);
}
if (window.Prism) return;
window.Prism = require("prismjs")
}
/**
* Sets event listeners for the RawViewer.
*/
RawViewer.prototype.setListener = function() {
var that = this;
$(document).on("objectSelected", () => {
this.clear();
});
$(document).on('cellInfoTabChange.RawViewer', function(e, target) {
console.log("tabChange", arguments);
if (target !== "previewRawData") that.clear();
})
$(document).off("onAfterSelectCell.RawViewer");
$(document).on("onAfterSelectCell.RawViewer", function(e, selectionInfo) {
if (ui.cellInfoTab.getActiveTab() !== "previewRawData") return;
console.log("onAfterSelectedCell", arguments);
that.selectionInfo = selectionInfo;
engines.handler('onLoadSnippet').call(that, selectionInfo);
})
}
/**
* Adds text preview to the RawViewer.
* @param {object|string} textObj - The text object or string to be previewed.
* @param {object} [options] - Additional options.
* @param {number} [options.lineStart=1] - The line number to start the preview.
* @param {string} [options.language="html"] - The language of the text for syntax highlighting.
* @returns {Promise<jQuery>} - A promise resolving to the jQuery object of the added text preview.
*/
RawViewer.prototype.addTextPreview = async function(textObj, options) {
if (typeof textObj == "string") {
// text object that marked with boundary
textObj = {
textMarked:textObj,
boundary:""
}
}
console.log("generating preview with textObj:", textObj);
options = options || {};
options.lineStart = options.lineStart || 1;
options.language = options.language || "html";
var $template = $(`<pre class="snippetData line-numbers language-${options.language}" data-start="${options.lineStart}"><code class="language-${options.language}"></code></pre>`);
var keyString = this.getKeyString();
$template.find("code").text(textObj.textMarked);
this.container.append($template);
console.log("keyString", keyString);
var highlighted = $template.find("code").html();
if (textObj.boundary) {
console.log("replacing boundary", textObj);
highlighted = highlighted.replace(textObj.boundary, '<span class="highlight">');
highlighted = highlighted.replace(textObj.boundary, '</span>');
}
$template.find("code").html(highlighted);
await window.Prism.highlightAllUnder($template[0]);
if (options.data) {
for (var i in options.data)
$template.data(i, options.data[i]);
}
return $template
}
/**
* Gets the starting line number from the offset in the text.
* @param {string} text - The text.
* @param {number} offsetStart - The starting offset.
* @returns {number} - The starting line number.
*/
RawViewer.prototype.getStartingLineFromOffset = function(text, offsetStart) {
var before = text.substring(0, offsetStart).split('\n');
var margin = this.feather+1
if (before.length > margin) return before.length-margin;
return 1;
}
/**
* Gets a snippet of text from the given offsets in the text.
* @param {string} text - The text.
* @param {number} offsetStart - The starting offset.
* @param {number} [offsetEnd=offsetStart] - The ending offset.
* @returns {object} - The snippet of text.
*/
RawViewer.prototype.getSnippetFromOffset = function(text, offsetStart, offsetEnd) {
offsetEnd = offsetEnd || offsetStart;
var result = [];
var resultMarked = [];
var boundary= `----boundary:${(Math.random()+"").substring(2,10)}-----`;
var before = text.substring(0, offsetStart).split('\n');
var lineNum = before.length;
var current = text.substring(offsetStart, offsetEnd);
var after = text.substring(offsetEnd).split("\n");
var leftSide = before.pop()
var rightSide = after.shift()
var columnStart = leftSide.length;
var currentLine = leftSide+current+rightSide;
var currentLineMarked = leftSide+boundary+current+boundary+rightSide;
var starting = before.length-(1+this.feather);
if (starting < 0) starting = 0;
for (let i=starting; i<before.length; i++) {
if (i<0) break;
result.push(before[i]);
resultMarked.push(before[i]);
}
result.push(currentLine);
//resultMarked.push(boundary+currentLine+boundary);
resultMarked.push(currentLineMarked);
for (let i=0; i<this.feather; i++) {
if (i>after.length) break;
result.push(after[i]);
resultMarked.push(after[i]);
}
return {
text:result.join("\n"),
textMarked:resultMarked.join("\n"),
col:columnStart,
boundary:boundary,
line:lineNum
};
}
/**
* Checks if the RawViewer is clear.
* @returns {boolean} - Indicates whether the RawViewer is clear.
*/
RawViewer.prototype.isClear = function() {
if (this.container.find("*").length > 0) return false;
return true;
}
/**
* Clears the RawViewer.
*/
RawViewer.prototype.clear = function() {
this.container.empty();
}
/**
* Gets the key string from the selection info.
* @returns {string} - The key string.
*/
RawViewer.prototype.getKeyString = function() {
try {
return trans.getSelectedObject().data[this.selectionInfo.fromRow][trans.keyColumn]
} catch (e) {
//do nothing
}
}
/**
* Appends HTML content to the RawViewer container.
* @param {string} html - The HTML content to append.
* @returns {jQuery} - The container element after appending the HTML content.
*/
RawViewer.prototype.append = function(html) {
this.container.append($(html));
return this.container;
}
/**
* Opens a file asynchronously and reads its content.
* @param {string} path - The path of the file to open.
* @returns {Promise<string>} - A promise resolving to the content of the file.
*/
RawViewer.prototype.openFile = async function(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) return reject();
var thisEncoding = encoding.detect(data);
// if encoding is not detected, read with default writeEncoding
if (!thisEncoding) {
console.log("Unable to detect encoding via encoding.detect()");
thisEncoding = this.fileEncoding || "utf8";
}
console.log("detected encoding", thisEncoding);
var string;
try {
string = iconv.decode(data, thisEncoding);
} catch (e) {
console.warn("Unable to decode string try 'Shift_JIS'", e);
string = iconv.decode(data, 'Shift_JIS');
}
resolve(string);
})
})
}
/**
* Adds a text preview from a file offset to the RawViewer.
* @param {string} file - The path of the file.
* @param {number} offsetStart - The starting offset in the file.
* @param {number} offsetEnd - The ending offset in the file.
* @param {object} [options] - Additional options.
* @param {string} [options.language] - The language for syntax highlighting.
* @returns {Promise<jQuery>} - A promise resolving to the jQuery object of the added text preview.
*/
RawViewer.prototype.addTextPreviewFromFileOffset = async function(file, offsetStart, offsetEnd, options) {
options = options || {};
var fileContent;
if (this.cachedFileContentName !== file) {
//var fileContent = await common.fileGetContents(file);
fileContent = await this.openFile(file);
this.cachedFileContent = fileContent;
this.cachedFileContentName = file;
} else {
fileContent = this.cachedFileContent;
}
var snippetData = this.getSnippetFromOffset(fileContent, offsetStart, offsetEnd);
if (!options.language) {
var ext = nwPath.extname(file);
options.language = ext.substring(1);
}
options.lineStart = this.getStartingLineFromOffset(fileContent, offsetStart);
options.data = {
path:file,
};
var snippet = await this.addTextPreview(snippetData, options);
var wrapper = $(`<div class="snippetWrapper"><div class="snippetInfo flex"><span class="snippetPath">${file}</span><span class="lineNumber">Line:${snippetData.line}</span></div></div>`)
wrapper.append(snippet);
wrapper.data("fileInfo", {...options, ...{offsetStart:offsetStart, offsetEnd:offsetEnd}});
this.container.append(wrapper);
return wrapper;
}
/**
* Adds a text preview from a text offset to the RawViewer.
* @param {string} text - The text content.
* @param {number} offsetStart - The starting offset in the text.
* @param {number} offsetEnd - The ending offset in the text.
* @param {object} [options] - Additional options.
* @param {string} [options.language] - The language for syntax highlighting.
* @param {object} [options.data] - Additional data.
* @returns {Promise<jQuery>} - A promise resolving to the jQuery object of the added text preview.
*/
RawViewer.prototype.addTextPreviewFromTextOffset = async function(text, offsetStart, offsetEnd, options) {
options = options || {};
var snippetData = this.getSnippetFromOffset(text, offsetStart, offsetEnd);
options.lineStart = this.getStartingLineFromOffset(text, offsetStart);
var snippet = await this.addTextPreview(snippetData, options);
var wrapper = $(`<div class="snippetWrapper"><div class="snippetInfo flex"><span class="snippetPath">${options?.data?.path || ""}</span><span class="lineNumber">Line:${snippetData.line}</span></div></div>`)
wrapper.append(snippet);
wrapper.data("fileInfo", {...options, ...{offsetStart:offsetStart, offsetEnd:offsetEnd}});
this.container.append(wrapper);
return wrapper;
}
/**
* @param {Object} selectedCell - Selected cell object passed from event onLoadSnippet
* @param {String} language - language of the snippet, determines syntax highlight
* @param {Object[]} offsetList - Array of object (because one row can have more than one occurance of string)
* @param {integer} offsetList[].start - Offset start of the string
* @param {integer} offsetList[].end - Offset end of the string
* @param {string} [filePath] - Path to the file. By default the file in the original project location / cache.
*/
RawViewer.prototype.commonHandleFile = async function(selectedCell, language, offsetList, filePath) {
console.log("commonHandleFile");
console.log("selected cell:", selectedCell);
var obj = trans.getSelectedObject();
if (!Array.isArray(obj.context[selectedCell.fromRow])) return;
if (obj.context[selectedCell.fromRow].length == 0) return;
console.log("determining active path");
var activePath = filePath || nwPath.join(trans.project.loc, obj.path);
if (await common.isFileAsync(activePath) == false) activePath = nwPath.join(trans.getStagingDataPath(), obj.path);
if (await common.isFileAsync(activePath) == false) {
this.clear();
console.log("no active path");
let $warningMsg = $(`<div class="blockBox warningBlock withIcon">${t(`Unable to find the file related to the data`)}<br />
${t(`To fix this correct the <b>Raw material location</b> field at the <a href="#" class="openProjectProperties">Project properties</a>`)}</div>`)
$warningMsg.find(".openProjectProperties").on("click", function() {
ui.openProjectProperties();
});
this.append($warningMsg)
return;
}
console.log("active path is:", activePath);
this.clear();
offsetList = offsetList || obj.parameters[selectedCell.fromRow]
for (var i in offsetList) {
var thisParam = offsetList[i]
if (!thisParam.start) continue;
var previewOptions = {
language:language,
externalEditor:true
}
var elm = await this.addTextPreviewFromFileOffset(activePath, thisParam.start, thisParam.end, previewOptions);
this.addContextMenu(elm, {...thisParam, ...{path:activePath}}, previewOptions);
elm.data("path", activePath)
}
}
/**
* Handles attachment content by adding text previews with context menus.
* @param {object} selectedCell - The selected cell object.
* @param {string} language - The language for syntax highlighting.
* @param {Array<object>} [offsetList] - The list of offsets.
*/
RawViewer.prototype.handleAttachment = async function(selectedCell, language, offsetList) {
console.log("commonHandleFile");
console.log("selected cell:", selectedCell);
var obj = trans.getSelectedObject();
if (!Array.isArray(obj.context[selectedCell.fromRow])) return;
if (obj.context[selectedCell.fromRow].length == 0) return;
this.clear();
offsetList = offsetList || obj.parameters[selectedCell.fromRow]
for (var i in offsetList) {
var thisParam = offsetList[i]
if (!thisParam.start) continue;
let attachmentContent = trans.getAttachmentContent(thisParam.attachment);
if (!attachmentContent) continue;
let activePath = "attachment:"+thisParam.attachment
var previewOptions = {
language:language,
data: {
path:activePath
}
}
var elm = await this.addTextPreviewFromTextOffset(trans.getAttachmentContent(thisParam.attachment), thisParam.start, thisParam.end, previewOptions);
this.addContextMenu(elm, {...thisParam, ...{path:activePath}}, previewOptions);
elm.data("path", activePath)
}
}
/**
* Adds a context menu to the specified element.
* @param {HTMLElement} elm - The element to which the context menu will be added.
* @param {object} obj - An object.
* @param {object} [options={}] - Additional options.
* @param {boolean} [options.externalEditor=false] - Whether to enable options for opening with external editors.
*/
RawViewer.prototype.addContextMenu = function(elm, obj, options={}) {
//var that = this;
elm = elm || this.elm
var menu;
var createMenu = () => {
menu = new nw.Menu();
// Add some items with label
menu.append(new nw.MenuItem({
label: t('Copy path to clipboard'),
type: "normal",
click: function(){
var filePath = elm.data("path");
if (!filePath) return;
var clipboard = nw.Clipboard.get();
clipboard.set(nwPath.resolve(filePath));
alert(t("Path has been copied to clipboard."));
}
}));
if (!options.externalEditor) return;
menu.append(new nw.MenuItem({
label: t('Open folder'),
type: "normal",
click: function(){
var filePath = elm.data("path");
if (!filePath) return;
nw.Shell.showItemInFolder(nwPath.resolve(filePath));
}
}));
menu.append(new nw.MenuItem({
label: t('Open with external application'),
type: "normal",
click: function(){
var filePath = elm.data("path");
if (!filePath) return;
nw.Shell.openItem(nwPath.resolve(filePath));
}
}));
var openWithMenu = new nw.Menu();
//opacity.append(new nw.MenuItem({ type: 'separator' }));
openWithMenu.append(new nw.MenuItem({
label: t('VS Code'),
type: "normal",
click: async function(){
var scriptInfo = elm.data()
var content = ""
try {
content = await common.fileGetContents(scriptInfo.path);
} catch (e) {
return alert("Error opening file:", scriptInfo.path)
}
var pos = common.cursorPosInfo(content, scriptInfo.fileInfo.offsetStart);
common.aSpawn("code", ["--goto", `"${scriptInfo.path}":${pos.row}`], {
detached:true,
shell:true
})
}
}));
openWithMenu.append(new nw.MenuItem({
label: t('Notepad++'),
type: "normal",
click: async function(){
var scriptInfo = elm.data()
var content = ""
try {
content = await common.fileGetContents(scriptInfo.path);
} catch (e) {
return alert("Error opening file:", scriptInfo.path)
}
var pos = common.cursorPosInfo(content, scriptInfo.fileInfo.offsetStart);
common.aSpawn("start", ["notepad++", `"${scriptInfo.path}"`, `-n${pos.row}`], {
detached:true,
shell:true
})
}
}));
menu.append(new nw.MenuItem({
label: t('Open with...'),
submenu: openWithMenu
}));
}
elm.on("contextmenu", function(ev) {
ev.preventDefault();
createMenu();
menu.popup(parseInt(ev.originalEvent.x), parseInt(ev.originalEvent.y));
return false;
})
}
/**
* Initializes the RawViewer by setting event listeners and loading Prism.
*/
RawViewer.prototype.init = function() {
$("head").append(`
<style>
#previewRawData .blockBox {
background-color: #fff;
margin: 12px;
}
</style>
`)
this.setListener();
this.loadPrism();
}
exports.RawViewer = RawViewer;