﻿/*
 * line-numbers.jsx
 * 
 * 
 * This script adds line numbers to text frames.
 * 
 * Author: Martin Kraetke (Twitter: @mkraetke)
 *
 * 
 * LICENSE
 * 
 * Copyright (c) 2015, le-tex publishing services GmbH
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 */
//@targetengine "session"

options = {
    debug:1,
    styles:[],
    restartNumPage:1,
    restartNumFrame:1,
    ignoreEmptyLines:1,
    applyToStyle:0,
    applyTo:0,
    interval:1,
    intervalFirstLine:0,
    startFrom:1,
    counter:0,
    pageOffset: app.documents[0].pages[0].documentOffset, // start with 1st page
    lineNumberObjStyleName: "Obj_LineNumber",
    lineNumberParaStyleName: "Para_LineNumber"
}

panel = {
    title:"le-tex – Insert Line Numbers",
    stylesTitle:"Select Paragraph Styles",
    optionsTitle:"Options",
    applyTitle:"Apply to",
    startTitle:"Start at",
    intervalTitle:"Interval",
    intervalFirstLineTitle:"Start numbering with first line",
    restartNumPageTitle:"Restart numbering each page",
    restartNumFrameTitle:"Restart numbering each frame",
    ignoreEmptyLinesTitle:"Ignore empty lines",
    radioDocumentTitle:"Document",
    radioStoryTitle:"Selected Story",
    radioTextFrameTitle:"Selected Text Frame",
    lineNumberStylesTitle:"Line Number Styles",
    lineNumberObjStyleTitle:"Object Style",
    lineNumberParaStyleTitle:"Paragraph Style",
    buttonAddNumbersTitle:"Add Numbers",
    buttonRemoveNumbersTitle:"Remove all numbers",
    buttonCancelTitle:"Cancel",
    successMessage:" line numbers inserted.",
    removeMessage:" line numbers removed.",
}

main();

function main(){
    // open file dialog and load a document
    if (app.layoutWindows.length == 0) {
        var file = File.openDialog ("Select a file", "InDesign:*.indd;*.indb;*.idml, InDesign Document:*.indd, InDesign Book:*.indb, InDesign Markup:*.idml", true)
        try {
            app.open(File(file));
        } catch (e) {
            alert(e);
            return;
        };
    }
    var doc = app.documents[0];
    // check if document is saved
    if ((!doc.saved || doc.modified)) {
        if ( confirm ("The document needs to be saved.", undefined, "Document not saved.")) {
            try {
                var userLevel = app.scriptPreferences.userInteractionLevel;
                app.scriptPreferences.userInteractionLevel = UserInteractionLevels.INTERACT_WITH_ALL;
                doc = doc.save();
                app.scriptPreferences.userInteractionLevel = userLevel;
            } catch (e) {
                alert ("The document couldn't be saved.\n" + e);
                return;
            }
        }
        else {
            return;
        }
    }
    // show window
    try {
        drawWindow(doc);
    } catch (e) {
        alert ("Error:\n" + e);
    }
}

function drawWindow (doc) {

    var paraStyles = app.documents[0].allParagraphStyles.sort();                

    var myWindow = new Window ("palette", panel.title, undefined);
    myWindow.orientation = "row";
    myWindow.alignChildren = "fill";
    // select styles panel
    var stylesGroup = myWindow.add ("group {alignChildren: ['left', 'fill']}");
    var stylesGroupPanel = stylesGroup.add ("panel", undefined, panel.stylesTitle);
    var stylesGroupList = stylesGroupPanel.add ("listbox", undefined, undefined, {multiselect: true});
    stylesGroupList.alignment = "fill";
    stylesGroupList.preferredSize = [200, 325];
    
    for (i = 0; i < paraStyles.length; i++){
        if(i < paraStyles.length && paraStyles[i].name != options.lineNumberParaStyleName){
            var listitem = stylesGroupList.add ("item");
            listitem.text = paraStyles[i].name;
        }
    }
    // options panel
    var optionsGroup = myWindow.add ("group {orientation: 'column'}");
    var optionsGroupPanel = optionsGroup.add ("panel", undefined, panel.optionsTitle);
    optionsGroupPanel.alignment = "fill";
    optionsGroupPanel.alignChildren = "fill";
    var optionsGroupPanelStartGroup = optionsGroupPanel.add ("group {orientation: 'row'}");
    var optionsGroupPanelInputStart = optionsGroupPanelStartGroup.add ("edittext", undefined, options.startFrom);
    optionsGroupPanelInputStart.characters = 3;
    optionsGroupPanelStartGroup.add ("statictext", undefined, panel.startTitle);    
    var optionsGroupPanelIntervalGroup = optionsGroupPanel.add ("group {orientation: 'row'}");
    var optionsGroupPanelInputInterval = optionsGroupPanelIntervalGroup.add ("edittext", undefined, options.interval);
    optionsGroupPanelInputInterval.characters = 3;
    optionsGroupPanelIntervalGroup.add ("statictext", undefined, panel.intervalTitle);
    var optionsGroupPanelInputIntervalFirstLine = optionsGroupPanel.add ("checkbox", undefined, panel.intervalFirstLineTitle);
    optionsGroupPanelInputIntervalFirstLine.value = 1;
    optionsGroupPanelInputIntervalFirstLine.enabled = optionsGroupPanelInputInterval.text == "1" ? false : true;
    optionsGroupPanelInputInterval.onChanging = function(){
        if(optionsGroupPanelInputInterval.text == "1"){
            optionsGroupPanelInputIntervalFirstLine.enabled = false;
            optionsGroupPanelInputIntervalFirstLine.value = 1;
        }else{
            optionsGroupPanelInputIntervalFirstLine.enabled = true;
        }
    }
    var optionsGroupPanelRestartNumPage = optionsGroupPanel.add ("checkbox", undefined, panel.restartNumPageTitle);
    optionsGroupPanelRestartNumPage.value = options.restartNumPage;
    optionsGroupPanelRestartNumPage.enabled = (options.restartNumFrame == 1) ? false : true;
    var optionsGroupPanelRestartNumFrame = optionsGroupPanel.add ("checkbox", undefined, panel.restartNumFrameTitle);
    optionsGroupPanelRestartNumFrame.value = options.restartNumFrame;
    optionsGroupPanelRestartNumFrame.onClick = function(){
        if(optionsGroupPanelRestartNumFrame.value == 1){
            optionsGroupPanelRestartNumPage.enabled = false;
            optionsGroupPanelRestartNumPage.value = 1;
        }else{
            optionsGroupPanelRestartNumPage.enabled = true;
        }
    }
    var optionsGroupPanelIgnoreEmptyLines = optionsGroupPanel.add ("checkbox", undefined, panel.ignoreEmptyLinesTitle);
    optionsGroupPanelIgnoreEmptyLines.value = options.ignoreEmptyLines;
    // panel apply to 
    var optionsGroupApplyToGroup = optionsGroup.add ("panel", undefined, panel.applyTitle);
    optionsGroupApplyToGroup.alignment = "fill";
    optionsGroupApplyToGroup.alignChildren = "left";
    optionsGroupApplyToGroup.add ("radiobutton", undefined, panel.radioDocumentTitle);
    optionsGroupApplyToGroup.add ("radiobutton", undefined, panel.radioStoryTitle);
    optionsGroupApplyToGroup.add ("radiobutton", undefined, panel.radioTextFrameTitle);
    optionsGroupApplyToGroup.children[options.applyTo].value =true;
    // panel style names
    var optionsGroupPanelLineNumberStyles = optionsGroup.add ("panel", undefined, panel.lineNumberStylesTitle);
    optionsGroupPanelLineNumberStyles.orientation = "column";
    optionsGroupPanelLineNumberStyles.alignment = "fill";
    optionsGroupPanelLineNumberStyles.alignChildren = "fill";
    var optionsGroupPanelLineObjStyleGroup = optionsGroupPanelLineNumberStyles.add("group");    
    var lineNumberObjStyleInput = optionsGroupPanelLineObjStyleGroup.add ("edittext", undefined, options.lineNumberObjStyleName);
    var optionsGroupPanelParaStyle = optionsGroupPanelLineObjStyleGroup.add ("statictext", undefined, panel.lineNumberObjStyleTitle);
    lineNumberObjStyleInput.characters = 20;
    var optionsGroupPanelLineParaStyleGroup = optionsGroupPanelLineNumberStyles.add("group");
    var lineNumberParaStyleInput = optionsGroupPanelLineParaStyleGroup.add ("edittext", undefined, options.lineNumberParaStyleName);
    var optionsGroupPanelParaStyle = optionsGroupPanelLineParaStyleGroup.add ("statictext", undefined, panel.lineNumberParaStyleTitle);

    lineNumberParaStyleInput.characters = 20;
    // button group    
    var buttonGroup = myWindow.add ("group {orientation: 'column', alignChildren: 'fill'}");
    var buttonAddNumbers = buttonGroup.add("button", undefined, panel.buttonAddNumbersTitle);
    buttonAddNumbers.enabled = true;
    var buttonRemoveNumbers = buttonGroup.add("button", undefined, panel.buttonRemoveNumbersTitle);
    var buttonCancel = buttonGroup.add("button", undefined, panel.buttonCancelTitle);
    buttonCancel.alignment = ["fill","bottom"];
    buttonAddNumbers.onClick = function(){

        options.styles = stylesGroupList.selection;
        options.restartNumPage = optionsGroupPanelRestartNumPage.value;
        options.restartNumFrame = optionsGroupPanelRestartNumFrame.value;
        options.ignoreEmptyLines = optionsGroupPanelIgnoreEmptyLines.value;
        options.startFrom = parseInt(optionsGroupPanelInputStart.text);
        options.interval = parseInt(optionsGroupPanelInputInterval.text);
        options.intervalFirstLine = optionsGroupPanelInputIntervalFirstLine.value;
        options.applyTo = selectedRadio(optionsGroupApplyToGroup);
        options.lineNumberObjStyleName = lineNumberObjStyleInput.text;
        options.lineNumberParaStyleName = lineNumberParaStyleInput.text;
        // intialize line numbering
        if(options.styles == null){
            alert("Please select one or more paragraph styles.")
        } else {
            try{
                addNumbers(options);
                alert(options.counter + panel.successMessage);
            } catch(e){
                alert(e);
            }
        }
    }
    buttonRemoveNumbers.onClick = function(){
        options.lineNumberObjStyleName = lineNumberObjStyleInput.text;
        removeNumbers(options);
    }

    buttonCancel.onClick = function() {
        myWindow.close();
    }
    return myWindow.show();
}

function removeNumbers(options){
    var doc = app.activeDocument;
    var stories = doc.stories;
    var counter = 0;
    for (i = 0; i < stories.length; i++){
        var currentStory = stories[i];
        var textFrames = currentStory.textFrames.everyItem().getElements();
        for (j = 0; j < textFrames.length; j++){
            var currentTextFrame = textFrames[j];
            if(currentTextFrame.appliedObjectStyle.name == options.lineNumberObjStyleName){
                currentTextFrame.remove();
                counter = counter +=1
            }
        }
    }
    alert(counter + panel.removeMessage)
    return;
}


function addNumbers(options) {
    var doc = app.activeDocument;
    // startFromTemp needed for saving last line number of frame 
    options.startFromTemp = options.startFrom;

    // create styles, later they are applied
    if(!doc.objectStyles.itemByName(options.lineNumberObjStyleName).isValid){
        var lineNumberObjStyle = doc.objectStyles.add();
        lineNumberObjStyle.name = options.lineNumberObjStyleName;
    }
    if(!doc.paragraphStyles.itemByName(options.lineNumberParaStyleName).isValid){
        var lineNumberParaStyle = doc.paragraphStyles.add();
        lineNumberParaStyle.name = options.lineNumberParaStyleName;
    }
    // add line numbers to entire document
    if(options.applyTo == 0){
        var scope = addNumbersToDocument(doc, options);
    // .. to story
    } else if (options.applyTo == 1){
        var scope = addNumbersToStory(doc, app.selection[0].parentStory, options);
    // .. to text frame
    } else if(app.selection[0] && app.selection[0].constructor.name == "TextFrame") {
        var scope = addNumbersToTextFrame(doc, app.selection[0], options);
    } else if(app.selection[0] && app.selection[0].hasOwnProperty("parentTextFrames") &&  app.selection[0].parentTextFrames.length == 1 ) {
        var scope = addNumbersToTextFrame(doc, app.selection[0].parentTextFrames[0], options);
    } else {
        return alert("No Text Frame selected.");
    }
    return;
}

function addNumbersToDocument(doc, options) {
    var stories = doc.stories;
    for (i = 0; i < stories.length; i++){
        currentStory = stories[i];
        addNumbersToStory(doc, currentStory, options);
    }
}

function addNumbersToStory(doc, story, options){
    var textContainers = story.textContainers;
    for (j = 0; j < textContainers.length; j++){
        currentTextFrame = textContainers[j];
        // exclude all frames with a line number object style
        if(currentTextFrame.appliedObjectStyle.name != options.lineNumberObjStyleName && currentTextFrame.parentPage != null){
            addNumbersToTextFrame(doc, currentTextFrame, options);
        }
    }
}

function addNumbersToTextFrame(doc, textFrame, options){
    var currentPageOffset = textFrame.parentPage.documentOffset;
    var pageBreakBoolean = currentPageOffset != options.pageOffset;
    options.pageOffset = currentPageOffset;
    var lines = textFrame.lines;
    var penalty = 0;
    // discontinue line numbering at page breaks when corresponding option is set
    var start = (pageBreakBoolean && options.restartNumPage == 1) ? options.startFrom : options.startFromTemp;

    for (k = 0; k < lines.length; k++){
        var currentLine = lines[k];
        var lineParaStyle = currentLine.appliedParagraphStyle.name;

        // penalty for empty lines
        penalty = ((!inArray(lineParaStyle, options.styles) &&  ( !(isLineEmpty(currentLine.contents) && options.ignoreEmptyLines == 1)))  
            || (isLineEmpty(lines[k-1].contents) && options.ignoreEmptyLines == 1) 
            ) ? penalty +=1 : penalty;

        // calculate line number
        var lineNumber = k + start - penalty;
        
        // remember last line if numbering should be contiuned for next frame
        options.startFromTemp = (options.restartNumFrame == 1) ? options.startFromTemp : lineNumber + 1;
        var lineNumberStr = lineNumber.toString();

        if(!Boolean(isLineEmpty(currentLine.contents) && options.ignoreEmptyLines == 1) 
          && inArray(lineParaStyle, options.styles)
           && (lineNumber % options.interval == 0) || (lineNumber == 1 && options.intervalFirstLine == 1)){

            options.counter = options.counter += 1
	    
	    // place insertion point depending on blank lines
	    var anchoredFrame = (currentLine.contents.match(/^\s*$/) == null) ? currentLine.insertionPoints[1].textFrames.add() : currentLine.insertionPoints[0].textFrames.add();

            anchoredFrame.contents = lineNumberStr;
            anchoredFrame.paragraphs[0].appliedParagraphStyle = doc.paragraphStyles.itemByName(options.lineNumberParaStyleName);

            fitTextFrameProportionally(anchoredFrame, 0.01);
            anchoredFrame.fit(FitOptions.FRAME_TO_CONTENT);

            // apply object style
            anchoredFrame.appliedObjectStyle = doc.objectStyles.itemByName(options.lineNumberObjStyleName);

            // anchored object settings, can be overwritten with object style
            var anchoredFrameObjSettings = anchoredFrame.anchoredObjectSettings
            anchoredFrameObjSettings.anchoredPosition = AnchorPosition.ANCHORED;
            anchoredFrameObjSettings.horizontalReferencePoint = AnchoredRelativeTo.TEXT_FRAME;
            anchoredFrameObjSettings.horizontalAlignment = HorizontalAlignment.LEFT_ALIGN;
            anchoredFrameObjSettings.verticalReferencePoint = VerticallyRelativeTo.LINE_BASELINE;
            anchoredFrameObjSettings.verticalAlignment = VerticalAlignment.BOTTOM_ALIGN;
            // place relative to spine
            anchoredFrameObjSettings.spineRelative = true;
            anchoredFrameObjSettings.anchorXoffset = 5;
            anchoredFrameObjSettings.anchorYoffset = 0;

            anchoredFrame.strokeWeight = 0;

        }
        
    }
    //return;
}

// return array from groups with children input elements
function selectedRadio (inputGroup) {
    var arr = []
    for(var i = 0; i < inputGroup.children.length; i++) {
        if(inputGroup.children[i].value == true){
            arr.push(i);
        }
    }
    return arr;
}

function inArray(item,array){
    for(l=0; l < array.length; l++){
        if( array[l].toString() === item){
            return true;
        }
    }
    return false;
}

function isLineEmpty(line){
    if(line.match(/^(\n|\s)*$/)){
        return true;
    } else {
        return false;
    }
}

function resize(textFrame, by) {  
    textFrame.resize(CoordinateSpaces.INNER_COORDINATES, AnchorPoint.TOP_LEFT_ANCHOR, ResizeMethods.MULTIPLYING_CURRENT_DIMENSIONS_BY, [by, by]);  
}

function fitTextFrameProportionally(textFrame, factor) {  
    if (textFrame.overflows) {  
       while (textFrame.overflows) {  
            resize(textFrame, 1 + factor);  
       }  
    }  
    else {  
       while (!textFrame.overflows) {  
            resize(textFrame, 1 - factor);  
       }  
       resize(textFrame, 1 / (1 - factor));  
    }  
}
