js/CommentParser.js

const commentParser = require("comment-parser");
const jsonata = require("jsonata");

const CommentParser = function(script, workspace="info") {
    this.script = script;
    this.workspace = workspace;

    this.comments = commentParser.parse(script, {spacing: "preserve"});

    var parsedScript;

    console.log(this.comments);
    this.getAll = () => {
        return this.comments;
    }
    
    const normalizeNextLines = (texts) => {
        let lines = texts.split("\n");
        const result = []
        result.push(lines[0])
        for (let i=1; i<lines.length; i++) {
            result.push(" * "+lines[i]);
        }
        return result.join("\n")
    }
    this.normalizeNextLines = normalizeNextLines;

    const getWorkspaceString = ()=> {
        if (!this.workspace) return "";
        return ":"+this.workspace;
    }
    const stringifyComment = (commentPart)=> {
        if (typeof commentPart !== "object") return "";
        if (!commentPart?.description && !commentPart?.tags?.length) return "";
        console.log("Generating string from", commentPart);
        let lines = [];
        commentPart.description ||= "";
        lines.push(`/**`+normalizeNextLines(getWorkspaceString()+"\n"+commentPart.description));

        if (commentPart.tags?.length) {
            for (let i in commentPart.tags) {
                let thisTag = commentPart.tags[i];
                if (!thisTag) continue;
                
                let name = thisTag.name;
                if (thisTag.default) name = `${thisTag.name}=${thisTag.default}`;
                if (thisTag.optional) name = `[${name}]`;
                lines.push(` * @${thisTag.tag} ${name} ${normalizeNextLines(thisTag.description)}`)
            }
        }
        lines.push(` */`)
        return lines.join("\n")
    }

    const normalizeCommentObj = (commentObj) => {
        if (!commentObj) return commentObj;
        if (!commentObj.description) return commentObj;
       
        let descLine = commentObj.description.split("\n");
        let currentWorkspace = descLine.shift();
        commentObj.description = descLine.join("\n");
        commentObj.workspace = currentWorkspace.trim();
        return commentObj;
    }

    this.hasComment = () => {
        if (this.comments?.length) return true;
        return false;
    }

    this.appendComment = (infoObj) => {
        if (typeof infoObj !== "object") return console.warn("Invalid type infoObj", infoObj);
        if (!infoObj?.description && !infoObj?.tags?.length) return console.warn("InfoObj must at least has description property or at least one tags property", infoObj);
        let script = stringifyComment(infoObj);
        console.log("Info string:", script);
        return this.appendCommentString(script);
    }

    /**
     * Append comment string to the begining of the script
     * @param {String} script 
     * @returns {Object[]} - Array of the appended comment object
     */
    this.appendCommentString = (script) => {
        const parsedObj = commentParser.parse(script, {spacing: "preserve"});
        if (!parsedObj?.length) return;
        this.comments ||= [];

        let pool = []
        var result = []
        for (let i=0; i<parsedObj.length; i++) {
            parsedObj[i] = normalizeCommentObj(parsedObj[i]);
            let newLen = this.comments.push(parsedObj[i]);
            let currentID = newLen - 1;
            let placeholderStr = `/*-commentPlaceholder_${currentID}-*/`;
            this.placeholders[currentID] = placeholderStr;
            pool.push(placeholderStr);
            result.push(parsedObj[i])
        }

        parsedScript = pool.join("\n")+"\n"+parsedScript;
        return result;
    }

    this.editOrCreateOneTag = (tagInformation) => {
        console.log("editOrCreateOneTag", tagInformation);
        if (typeof tagInformation !== "object") return console.warn("Invalid commentObj", tagInformation);
        if (!tagInformation?.tag) return console.warn("Required tag properties don't exist", tagInformation);
        if (!this.hasComment()) {
            this.appendComment({
                tags: [tagInformation]
            });
            return;
        } 

        // edit existing tag
        var thisComments = this.getAll();
        for (let i in thisComments) {
            if (!thisComments[i]?.tags) continue;
            for (let tagId in thisComments[i].tags) {
                if (thisComments[i].tags[tagId]?.tag == tagInformation.tag) {
                    thisComments[i].tags[tagId] = tagInformation;
                    return;
                }
            }
        }

        // find no existing tag. Append one;
        thisComments[0].tags.push(tagInformation);
    }

    this.getDescriptionByTag = (tag) => {
        if (!this.comments?.length) return [];
        var result = [];
        for (let i=0; i<this.comments.length; i++) {
            if (!this.comments[i]?.tags?.length) continue;
            for (let y=0; y<this.comments[i].tags.length; y++) {
                if (this.comments[i].tags[y].tag == tag) result.push(this.comments[i].tags[y].description);
            }
        }
        return result;
    }

    this.getTags = (tag) => {
        if (!this.comments?.length) return [];
        var result = [];
        for (let i=0; i<this.comments.length; i++) {
            if (!this.comments[i]?.tags?.length) continue;
            for (let y=0; y<this.comments[i].tags.length; y++) {
                if (this.comments[i].tags[y].tag == tag) result.push(this.comments[i].tags[y]);
            }
        }
        return result;
    }

    // this.stringify = ()=> {
    //     if (!this.comments?.length) return this.script;
    //     var thisParsedScript = parsedScript;
    //     for (let i=0; i<this.comments.length; i++) {
    //         if (!this.comments[i]) continue;
    //         thisParsedScript = thisParsedScript.replace(`/*-commentPlaceholder_${i}-*/`, stringifyComment(this.comments[i]))
    //     }
    //     return thisParsedScript;
    // }
    this.stringify = ()=> {
        if (!this.comments?.length) return this.script;
        var thisParsedScript = parsedScript;
        for (let i in this.placeholders) {
            if (!this.comments[i]) continue;
            thisParsedScript = thisParsedScript.replace(this.placeholders[i], stringifyComment(this.comments[i]))
        }
        return thisParsedScript;
    }

    this.query = async (query)=> {
        const expr = jsonata(query);
        return await expr.evaluate(this.comments);
    }
    
    this.placeholders = {};


    this.parse = () => {
        parsedScript = this.script;
        let infos = this.comments;
        console.log("infos", infos);
        this.parsed = []
        let currentID = 0;
        for (let i=0; i<infos.length; i++) {
            if (!infos[i]) continue;
            if (!infos[i].description) continue;
            if (infos[i].description.substring(0, getWorkspaceString().length) !== getWorkspaceString()) continue;
            
            infos[i] = normalizeCommentObj(infos[i])

            let thisComment = infos[i];
            let stringify = commentParser.stringify(thisComment);
            this.placeholders[currentID] = `/*-commentPlaceholder_${currentID}-*/`;
            parsedScript = parsedScript.replace(stringify, this.placeholders[currentID]);
            currentID++;
        }
    }

    const init = ()=> {
        this.parse();
    }
    init();
}

module.exports = CommentParser;