1/ Dealing with Pages
Check whether Items are All Inside the Page Area
Notes on Resizing Individual Pages
What is the Nearest Page of some PageItem?
Why is PageItem.resize() Unit Independent?

2/ Characters, Text & Styles
Recursive Style Replacement Routine
How to Remove Empty Character Style Groups
Fit Horizontal Text Scale to Frame
Automatically Adjust the Size of a Bullet

3/ Various DOM Snippets
How to View the Code for Actions Assigned to Buttons
Change the Preferences of all Open Documents
Removing PathPoints from a Polygon

4/ On the ExtendScript Side
How do Enumerator Instances Mimick Numbers
What Strategy to Use for Error Messages?


1/ Dealing with Pages

Check whether Items are All Inside the Page Area

How to check that all available page items for a specific page remains within the page area?

At first sight this sounds like a simple “bounds inclusion” problem. First we implement a min-max routine to compute the area delimited by a set of page items, then we compare those data with the page bounds.

However, rounding errors may lead to wrong conclusions. In JavaScript, the usual machine epsilon is Math.pow(2,-53) but in InDesign metrics the floating-point precision is also impacted by internal conversions of measurement units, so I don't know the exact machine epsilon of InDesign coordinates. Empirically, one might consider that differences lower than 1e-10 are not perceptible—at least in points units. So when you compare coordinates, take into account this epsilon value.

Suggested code:

const E = 1e-10; // machine epsilon  
 
function getAllBounds(/*[t,l,b,r][]*/a)  
{  
    var t = 1/0, l = 1/0,  
        b = -1/0, r = -1/0,  
        i = a.length,  
        gb;  
 
    while( i-- )  
        {  
        gb = a[i];  
        gb[0] < t && (t=gb[0]); // top  
        gb[1] < l && (l=gb[1]); // left  
        gb[2] > b && (b=gb[2]); // bottom  
        gb[3] > r && (r=gb[3]); // right  
        }  
    return [t,l,b,r];  
}  
 
var myDocument = app.activeDocument;  
 
// Reset the origin *before* you grab bounds.  
// ---  
myDocument.zeroPoint = [0,0];  
 
var myPageBounds = myDocument.pages[0].bounds;  
 
// Using a selection doesn't help.  
// Compute min-max bounds instead.  
// ---  
var allBounds = getAllBounds(myDocument.pages[0]
    .pageItems.everyItem().geometricBounds);  
 
// Use machine epsilon (E) when comparing coordinates  
// ---  
var myDocumentReady = myPageBounds[0]-E <= allBounds[0]  
    && myPageBounds[1]-E <= allBounds[1]  
    && myPageBounds[2]+E >= allBounds[2]  
    && myPageBounds[3]+E >= allBounds[3];  
 
if( !myDocumentReady ) alert ("Items outside of page..."); 
 

• Original discussion: forums.adobe.com/message/7647972#7647972

Notes on Resizing Individual Pages

Resizing or rescaling pages individually does not affect DocumentPreference's pageWidth and pageHeight properties. Once a page size is changed, link is broken with document setup and users need to use the Page Tool to get informed about the actual size of a specific page. So if all pages are intended to have a uniform size, one should use myDoc.documentPreferences.properties = {pageWidth:myNewWidth, pageHeight:myNewHeight} instead.

Note 1. There is no point in changing myDoc.viewPreferences units before invoking myPage.resize(…) or any other transform method, as all values are expected in points.

Note 2. myDoc.pages.everyItem().resize(…) is always faster than an explicit loop.

Note 3. If the target pages have a non-empty master spread whose size must be inherited, mySpread.pages.everyItem().resize(…) might be a better and a safer approach. Otherwise, keep in mind that the modified pages and the corresponding master pages will be out of sync in terms of size, which can be hard to figure out once the job is done.

Note 4. The properties myPage.layoutRule and app.transformPreferences.whenScaling have critical consequences on how pages are resized and how these transformations (or deformations) are encoded in inner matrices. The actual size of a page as a device is not necessarily identical to the visible size of that page as a page item in the parent spread device. Those differences can be highlighted in comparing spread export (device=spread) and page export (device=page).

Other obscure issues regarding page size are discussed in this topic: Override elements by script problems

• See also: Coordinate Spaces and Transformations in Indesign (PDF)

• Original discussion: forums.adobe.com/message/7597545#7597545

What is the Nearest Page of some PageItem?

Mac wrote: “I have a InDesign file with a spread that has two pages, each page has it's own slug with page related information now what I need to get page object pertaining to that slug. As we all know the parentPage property returns null incase of pageItem is outside the page trim area…”

One can, however, identify the nearest page of some given page item. Just consider the parent spread of the object and loop over its specific pages. For each page we will compute center-to-center distance along the x-axis and keep the best match—that is, the lowest value—as shown below:

var parentSpread = function F(/*DOM*/o)  
//--------------------------------------  
// Get the parent spread of this DOM object, or NULL.  
{  
    var p = o && o.parent;  
    if( (!p) || (p instanceof Document) ) return null;  
    return ( (p instanceof Spread) || (p instanceof MasterSpread) ) ? p : F(p);  
};  
 
var getProxyPageName = function(/*PageItem*/o)  
//--------------------------------------  
// Get the name of the page that either contains this object, or  
// is at the shortest distance of this object along the x-axis.  
{  
    const CS_SPREAD = +CoordinateSpaces.SPREAD_COORDINATES,  
          AP_CENTER = +AnchorPoint.centerAnchor,  
          mABS = Math.abs;  
 
    var x,p,a,d,i,t,iBest;  
 
    if( !(p=o.parentPage) && (p=parentSpread(o)) )  
        {  
        x = o.resolve(AP_CENTER,CS_SPREAD)[0][0];  
        for( d=1/0, a=p.pages.everyItem().getElements(), i=a.length ; i-- ; )  
            {  
            t = mABS(x - a[i].resolve(AP_CENTER,CS_SPREAD)[0][0]);  
            if( t < d ){ d=t; iBest=i; }  
            }  
        p = a[iBest];  
        }  
 
    return p ? p.name : '';  
};  
 
// Test  
// ---  
var o = app.properties.selection && app.selection[0];  
if( o ) alert( getProxyPageName(o) );
 

• Original discussion: forums.adobe.com/message/7679941#7679941

Why is PageItem.resize() Unit Independent?

All methods that explicitly deal with coordinate spaces and transformations expect dimensions and/or locations in points: resolve(), transform(), resize(), reframe().

There is just one exception for locations, provided that:
• coordinates are passed using the ruler system syntax;
• the param consideringRulerUnits is set to true.

In such case x and y are interpreted with respect to user units and custom zero-point.

What seems embarrassing is the fact that the ruler system is involved in many DOM object properties—PageItem.geometricBounds, PathPoint.anchor, and so on—while this is the most unsafe and volatile coordinate system in InDesign. Actual data are stored using coordinate spaces and transformation matrices (all in points units) as shown in IDML exports.

There is no problem in finding locations or computing sizes relative to a target coordinate space, but inversely it might be very fastidious to translate coordinate space data into the ruler system when the DOM API requires those ruler-related coordinates. A solution is to create your own transformation matrix. An example is shown at the end of the article Drawing Sine Waves in InDesign—see boxToRulerMatrix.

• Original discussion: forums.adobe.com/message/7715617#7715617


2/ Characters, Text & Styles

Recursive Style Replacement Routine

KateDdpc wrote: “Does anyone know if you can create a script that would delete and replace character styles? I have a list of styles that get brought into my InDesign document when I import the Word file. I'm trying to see if these can be deleted and replaced with a set of character styles existing in the indd doc.”

Noting that possible style groups might complicate the process, we suggest a recursive approach of the topic.

Note 1. — We access a style group within a style group using its own characterStyleGroups collection, as follows:
doc.characterStyleGroups.itemByName("MyGroup").
characterStyleGroups.itemByName("MySubGroup")... etc.

Note 2. — There is no critical difference between characterStyles and paragraphStyles in terms of data structure and API. So we can provide a generic routine, as shown below.

Suggested code:

var pathToStyle = function F(/*str[]*/path,/*obj*/parent,/*str*/pc,/*uint=0*/i,  s,t,g)  
// ---  
// Recursive access to a style based on its path  
// (utility of the replaceStyle function)  
{  
    i||(i=0);  
    g = +(i < path.length-1);  
    s = pc + (g ? 'StyleGroups' : 'Styles');  
    t = parent[s].itemByName(path[i]);  
 
    if( !t.isValid ) return null;  
    t = t.getElements()[0];  
 
    return g ? F(path,t,pc,1+i) : t;  
};  
 
var replaceStyle = function(/*p|c*/styleKind,/*str[]*/fromStylePath,/*str[]*/toStylePath,/*?Document*/doc,  s,fs,ts)  
// ---  
// styleKind :: use 'p' for paragraphStyle ; 'c' for characterStyle  
// ---  
// fromStylePath :: path of the style to be removed  
// toStylePath [opt.] :: path of the replacement style (default: no style)  
// Note 1: path format supports either a simple string (direct style name), or an  
//         array of strings ["groupName","subGroupName", ... ,"styleName"]  
// Note 2: in order to replace a paragraph style by the [Basic Paragraph] in a  
//         locale-independent way, set toStylePath to "$ID/NormalParagraphStyle"  
// ---  
// doc (opt.) :: the document to target (default: activeDocument)  
{  
    // Check styleKind  
    // ---  
    styleKind = styleKind ? (''+styleKind.toLowerCase()) : 'p';  
    if( 'p'!=styleKind && 'c'!=styleKind )  
        { throw Error("Wrong styleKind argument in replaceStyle()."); }  
    s = 'p'==styleKind ? 'paragraph' : 'character';  
 
    // Default doc  
    // ---  
    if( !(doc||(doc=app.properties.activeDocument)) )  
        { throw Error("No document available."); }  
 
    // Manage fromStyle  
    // ---  
    if( !fromStylePath )  
        { throw Error("Wrong fromStylePath argument in replaceStyle()."); }  
    (('object'==typeof fromStylePath) && fromStylePath.length) || (fromStylePath=[''+fromStylePath]);  
    fs = pathToStyle(fromStylePath,doc,s);  
    if( null===fs )  
        { throw Error("The fromStyle doesn't exist."); }  
 
    // Manage toStyle  
    // ---  
    if( !toStylePath )  
        {  
        // If toStylePath is missing or empty, then use either  
        // [None] style for character, or undefined for paragraph  
        // ---  
        ts = 'p'==styleKind ? undefined : parent.characterStyles.itemByName('$ID/[None]');  
        }  
    else  
        {  
        (('object'==typeof toStylePath) && toStylePath.length) || (toStylePath=[''+toStylePath]);  
        ts = pathToStyle(toStylePath,doc,s);  
        }  
    if( null===ts )  
        { throw Error("The toStyle doesn't exist."); }  
 
    // Replace  
    // ---  
    fs.remove(ts);  
};  
 
 
// =====================================  
// SAMPLE USAGE  
// =====================================  
 
var STYLE_KIND = 'p',  
    FROM_STYLE_PATH = ["Headings group","Heading 1 group","Style 1"],  
    TO_STYLE_PATH = "$ID/NormalParagraphStyle";  
 
replaceStyle(STYLE_KIND, FROM_STYLE_PATH, TO_STYLE_PATH);
 

• Original discussion: forums.adobe.com/message/4886454#4886454

How to Remove Empty Character Style Groups

Simple and useful routine:

var removeEmptyGroups = function F(/*parent*/p,  g)  
{  
    if( !(p=p.characterStyleGroups).length ) return;  
 
    p = p.everyItem().getElements();  
    while( g=p.pop() )  
        {  
        F(g);  
        if( !g.allCharacterStyles.length ) g.remove();  
        }  
};  
 
removeEmptyGroups(app.activeDocument); 
 

• Original discussion: forums.adobe.com/message/7696843#7696843

Fit Horizontal Text Scale to Frame

Given a set of one-line textframes, how to quickly “adjust the horizontal scale one at a time to fit the text frame,” with respect to min-max percentage factors.

The approach I recommend is based on seeking the optimal adjustment through a binary search. The code below is a generic implementation in that it allows you to apply the process with respect to any numeric property (horizontalScale, pointSize…) exposed by the TextFrame class:

// ---
// Select the text frames to be adjusted before you run the script.
// ---
 
(function(/*Item[]*/a,/*str*/k,/*uint*/MIN,/*uint*/MAX, /*num*/PRECISION)  
{  
    var t, s, v, w, m = [],  
        wrong = function(){ return +(t.overflows || 1 != s.lines.length) };  
 
    while( t=a.pop() )  
    {  
        if( 'TextFrame'!=t.__class__ ) continue;  
 
        // Init  
        // ---  
        v = (s=t.parentStory)[k];  
        m[0] = MIN;  
        m[1] = MAX;  
 
        // Try extremum and quit if status doesn't change  
        // ---  
        s[k] = m[1-(w=wrong())];  
        if( w==wrong() ){ s[k]=v; continue; }  
 
        // Binary search  
        // ---  
        while( m[1]-m[0] > PRECISION ){ m[w=wrong()] = s[k] = (m[0]+m[1])/2; }  
        w && (s[k] = m[0]);  
    }  
 
})(app.properties.selection||[], 'horizontalScale', 40, 200, .5); 
 

• Original discussion: forums.adobe.com/message/7415159#7415159

Automatically Adjust the Size of a Bullet

The goal of this exercise is funny: having two characters selected, a letter and a bullet, adjust the bullet size so that its visible height be a specific ratio (e.g. 50%) of the first character visible height.

Suggested code:

// Required ratio  
// ---  
const REQ_RATIO = .5;  // 50% (change it as needed)
 
// Boring constants  
// ---  
const CS_IN = +CoordinateSpaces.innerCoordinates,  
      IN_BOX = [+BoundingBoxLimits.geometricPathBounds,CS_IN],  
      TOP_LOC = [[0,0]].concat(IN_BOX),  
      BOT_LOC = [[0,1]].concat(IN_BOX);  
 
// Assuming two characters are selected  
// ---  
var sel = app.selection[0],  
    a = sel.characters.everyItem().createOutlines(false),  
    h = [], t, k;  
 
// Compute the heights => h[0],h[1]  
// ---  
while( t=a.pop() )  
    {  
    t = t[0];  
    h.unshift(t.resolve(BOT_LOC,CS_IN)[0][1]-t.resolve(TOP_LOC,CS_IN)[0][1]);  
    t.remove();  
    }  
 
// Apply the needed factor to the character scale  
// (or you could use pointSize, alternately)  
// ---  
k = REQ_RATIO*(h[0]/h[1]);  
sel.characters[1].verticalScale *= k;  
sel.characters[1].horizontalScale *= k;
 

Here is how the code works:

1. We compute both the visible height of the first character—say H0—and the visible height of the bullet—say HB—using the createOutlines trick.

2. Now, let k be the factor by which we'll have to change (i.e. rescale) the bullet height, HB, in order to meet the condition. The goal can be expressed as follows,
k × HB = ( 50 / 100 ) × H0.

3. Which leads to k = 0.5 × ( H0 / HB ).

4. Finally we change the scale of the bullet by k, working either on its verticalScale and horizontalScale properties, or on its pointSize. (I think scaling is more relevant here.)

• Original discussion: forums.adobe.com/message/7477876#7477876


3/ Various DOM Snippets

How to View the Code for Actions Assigned to Buttons

In order to inspect the actions (in fact, the behaviors) assigned to a Button, we need to parse the collection myButton.behaviors. It collects all kind of behaviors (GotoAnchorBehaviors, GotoFirstPageBehaviors, etc.). Each behavior then has a behaviorEvent property that indicates which mouse or focus event triggers the behavior.

There is no “actual code” in terms of event handlers, but one can explore and report sequentially the specific attributes of the behaviors.

Suggested code:

// Assuming a Button object is selected  
// ---  
var myButton = app.selection[0];  
 
 
// =================================
// FORMATTING UTILITIES  
// =================================
 
var formatBehaviorCode = function F(/*Behavior*/o,  a,k)  
// ---  
// Extract human-friendly 'code' from a Behavior of any kind  
{  
    F.SKIP||(F.SKIP={name:1,behaviorEvent:1,id:1,label:1,parent:1,index:1,/*add disregarded properties*/});  
 
    k = o.__class__;  
    o = o.properties;  
    a = [localize("%1(%2)",k,o.enableBehavior?"enabled":"disabled")];  
    delete o.enableBehavior;  
    for( k in o )  
        {  
        if( !o.hasOwnProperty(k) || F.SKIP[k] ) continue;  
        a.push(localize("%1(%2)",k,o[k]));  
        }  
    return a.join('; ');  
};  
 
var formatResults = function F(/*any*/o,/*?uint*/indent,  z,s,a,k)  
// ---  
// Format the results  
{  
    if( 'object' != typeof o ) return ''+o;  
 
    // Build the format string  
    // ---  
    s = '';  
    z = (indent||(indent=0));  
    while( z-- ) s+='    ';  
    s += "[%1]" + (indent?' ':'\r') + "%2";  
 
    // Explore the keys  
    // ---  
    a = [];  
    z = 0;  
    for( k in o )  
        {  
        if( !o.hasOwnProperty(k) ) continue;  
        a[z++] = localize( s, k, F(o[k],1+indent) );  
        }  
 
    // Join  
    // ---  
    return a.join(indent ? '\r' : '\r\r');  
};  
 
 
// =================================
// MAIN PROCESS  
// =================================
 
var a = myButton.behaviors.everyItem().getElements(),  
    r = {},  
    s, t, w, u;  
 
// Compute  
// ---  
while( t=a.shift() )  
    {  
    s = t.behaviorEvent.toString();  
    (r[s]||(r[s]=[])).push(formatBehaviorCode(t));  
    }  
 
// Display  
// ---  
(w=new Window('dialog',"Behaviors triggered from "+myButton.name))  
    .add('edittext',u,formatResults(r),{multiline:true}).parent  
    .add('button',u,"Close",{name:'OK'}).parent;  
w.children[0].minimumSize = [600,250];  
w.show();
 

Result:

Show Button's Behaviors.

• Original discussion: forums.adobe.com/message/7470582#7470582

Change the Preferences of all Open Documents

Say you want to change the measurement units, page size, slug offset, or any other set of preferred properties for all open documents.

The following template could help:

// Adjust the properties to your needs
// ---
app.documents.length && app.documents.everyItem().properties = {  
    viewPreferences:{  
        horizontalMeasurementUnits: +MeasurementUnits.MILLIMETERS,  
        verticalMeasurementUnits: +MeasurementUnits.MILLIMETERS,  
        cursorKeyIncrement: '1mm',  
        },  
    documentPreferences:  
        {  
        pageWidth: '200mm',  
        pageHeight: '250mm',  
        documentSlugUniformSize: true,  
        slugTopOffset: '10mm',  
        },  
    };
 

• Original discussion: forums.adobe.com/message/7684546#7684546

Removing PathPoints from a Polygon

Barbara wrote, “I have a closed polygon (single path) and want to remove two specific points—(myleft,mybottom) and (myright, mybottom)—from it. (…) But I don't understand how points are represented…”

Any Polygon (base class, SplineItem) is based on a collection of Paths, each path being itself a collection of PathPoints. The PathPoint structure exposes a remove() method so one can directly loop through polygon's pathpoints—from the end—in order to remove the unwanted points according to some criteria.

In the example discussed by Barbara, points to be removed must either have the coordinates [myleft,mybottom], or [myright, mybottom]myleft, myright, and mybottom being numeric values.

Technical Note. — On comparing [x,y] coordinates coming from InDesign rulers I strongly recommend you convert numbers into strings and base your code on string comparisons, because obscure rounding issues may occur due to measurement unit conversions which are undetectable using == or === on numbers. Someday I've encountered a weird case: myPathPoint.anchor[0] was returning some number (say 100) but it wasn't possible to validate the condition myPathPoint.anchor[0]==100 despite the fact that myPathPoint.anchor[0].toSource() was exactly returning Number(100). Coercing the value into a string solved the issue.

Suggested code:

// ---  
// Given poly (Polygon, no curve) and  
// myleft, myright, mybottom (Numbers)  
// ---  
 
var LB = [myleft, mybottom].join('|'),  
    RB = [myright, mybottom].join('|'),  
    myPathPoints = poly.paths[0].pathPoints.everyItem().getElements(),  
    i = myPathPoints.length, pp, xy;  
 
while( i-- )  
    {  
    xy = (pp=myPathPoints[i]).anchor.join('|');  
    if( xy==LB || xy==RB ) pp.remove();  
    }
 

• Original discussion: forums.adobe.com/message/7324871#7324871


4/ On the ExtendScript Side

How do Enumerator Instances Mimick Numbers

Enumerator instances do not have the cast Number—the typeof operator does return 'object'. What has been done when introducing these objects in CS5 is:
1. Making myEnum.valueOf() return a Number.
2. Overriding == and === so that myEnum can match a Number in both equal and strict-equal comparisons.

The reason for this was probably to preserve compatibility with previous versions (CS4 and before) where Enumeration—a collection of enums—was just pointing out to numeric values, that is, Number instances.

Thus, a syntax like (myUnit===MeasurementUnits.INCHES), or (myUnit===2053729891), or even (MeasurementUnits.INCHES===2053729891), still works from CS4 to CS5 without breaking existing code.

Now, about the meaning of these numbers, we know they are nothing but unique identifiers. Technically—and this is an old tradition in Adobe architecture—such identifiers are all built based on 4-character sequences.

In the case of measurement units, identifiers are formed using the prefix 'z' and three specific letters of the unit name, usually the leading letters, e.g. 'zinc' for inches, 'zpic' for picas, and so on.

Then, ASCII codes of these characters are taken to build a number:

INCHES => z i n c => 7A 69 6E 63 => 0x7A696E63 (=2053729891)
PICAS  => z p i c => 7A 70 69 63 => 0x7A706963 (=2054187363)
etc.
 

And one can check that +MeasurementUnits.INCHES is 0x7A696E63.

Now let's consider the following declarations:

var x = MeasurementUnits.picas;
var y = 2054187363;
var z = "2054187363";
 

We can check, first, that x===y is true (see reason 2 above). But we also can check that x===z is true! Which is not CS4 backwards-compatible at all and sounds highly irrelevant since x.toString()==='PICAS'.

I suspect that this strange case is a side effect of how the === operator has been overloaded. In my opinion, the hidden native function Enumerator['==='](arg) mutely coerces the argument into a Number whatever it actually is. So the string z is simply taken as +z and therefore x===z also returns true.

What Adobe should have done instead is:

// Pseudo code
Enumerator['==='] = function(arg)
    {
    return 'string'==typeof arg ?
          this.toString()==arg :
          this.valueOf()==+arg
    };
 

Then x===z would have been false, and x==='PICAS' would have been true.

• Original discussion: forums.adobe.com/message/7275471#7275471

What Strategy to Use for Error Messages?

I would make a clear distinction between messaging stuff, debugging tools and error management.

1. Messaging: prompting warnings, alerts, etc. to the user when something is wrong regarding the context from which s/he runs the script, or similar issues. E.g. "nothing is selected", "no document available" etc. These are not errors in the strict sense (although true errors may require messaging too.) To me, messaging is just an interface issue. It answers the question, how and when should I notify something special to the user? This shouldn't be strongly coupled with error management, because in most cases the bug is the user, not the script. A good script is responsible for detecting usage or context-related failures and should not throw exceptions in such cases.

2. Debugging: this is obviously a very broad question, which involves as well testing strategies, code quality tools (JSLint etc.), assertions, logging. All this stuff is clearly intended for the developer. Having in your toolbox a good logging library that you just need to include whenever a project becomes serious is the best approach to me. Logging features are somehow “embedded” in the final script, but they remain silent as long as the log level is zero. On the dev side you use various levels (e.g. trace, warning, error, critical…) during the design, but I suggest logging be also activatable from the user side—based on some hidden switcher—because users are expert in detecting issues. Having a verbose log report of all what happened before your script made InDesign crash is the most precious thing in the world.

3. Error management: this is the realm of “exceptions” and try…catch. That is, issues that shouldn't normally occur and would lead to runtime errors if they were not managed. ExtendScript provides exception handling and implements the basic Error types required by ECMA's specification. Note that error management is both connected to messaging (1) and debugging (2) but it seems to me very useful to consider it a separate domain. It answers the question, how, when and where do I manage actual errors? One side of error management is throwing custom exceptions. A good practice is to have some flag on your custom Error object so that you can identify that this was not a 'native' error. Exceptions should be thrown as close to where the error would have occurred (in order to recover the maximum amount of contextual data), but opinions are sharply divided about the place from where exceptions should be caught. A global try...catch embedding your whole script makes things very easy to manage, but there are less paranoid solutions. Either way, having a dedicated function (not only a “global variable”) for sending, storing, and receiving errors is, I think, the way to go.

• Original discussion: forums.adobe.com/message/7545219#7545219


GitHub page: github.com/indiscripts

Twitter stream: twitter.com/indiscripts

YouTube: youtube.com/user/IndiscriptsTV