# Object specifiers

## How object specifiers work

As explained in chapter 3, a property contains either a simple value describing an object attribute (`name`, `class`, `creationDate`, etc.) or an object specifier representing a one-to-one relationship between objects (e.g. `home`, `currentTrack`), while elements represent a one-to-many relationship between objects (`documents`, `folders`, `fileTracks`, etc). [TO DO: document class hierarchy as used in glues; note that PREFIXItem identifies a single property or element while PREFIXItems identifies zero or more elements, and summarize the selectors]

[TO DO: note that all properties and elements appear as read-only properties on glue-defined ObjectSpecifier and RootSpecifier subclasses; users don't instantiate Specifier classes directly but instead construct via chained property/method calls from glue's Application class or untargeted `RootSpecifier` constants (PREFIXApp, PREFIXCon, PREFIXIts)]

characters/words/paragraphs of documents by index/relative-position/range/filter

[TO DO: list of supported reference forms, with links to sections below]

[TO DO: following sections should include AppleScript syntax equivalents for reference]

## Reference forms

### Property

    var PROPERTY: PREFIXItem {get}

Examples:

    textedit.version
    textedit.documents[1].text
    finder.home.files.name

Syntax:

    specifier.property

### All elements

    var ELEMENTS: PREFIXItems {get}

Examples:

    finder.home.folders
    textedit.documents
    textedit.documents.paragraphs.words

Syntax:

    specifier.elements

### Element by index

    subscript(index: Any) -> PREFIXItem

Examples:

    textedit.documents[1]
    finder.home.folders[-2].files[1]

Syntax:

    elements[selector]

        selector : Int | Any -- the object's index (1-indexed), or other identifying value [1]

[1] While element indexes are normally integers, some applications may also accept other types (e.g. Finder's file/folder/disk specifiers also accept alias values). The only exceptions are `String` and `PREFIXSpecifier`, which are used to construct by-name and by-test specifiers respectively.

Be aware that index-based object specifiers always use one-indexing (i.e. the first item is 1, the second is 2, etc.), not zero-indexing as in Swift (where the first item is 0, the second is 1, etc.).

### Element by name

    subscript(index: String) -> PREFIXItem
    func named(_ name: Any) -> PREFIXItem

Examples:

    textedit.documents["Untitled"]
    finder.home.folders["Documents"].files["ReadMe.txt"]

Specifies the first element with the given name. (The subscript syntax is preferred; the `named` method would only need used if a non-string value was required.)

Syntax:

    elements[selector]
            selector : String -- the object's name (as defined in its 'name' property)

Applications usually treat object names as case-insensitive. Where multiple element have the same name, a by-name specifier only identifies the first element found with that name. (To identify all elements with a particular name, use a by-test specifier instead.)

[TO DO: update once a final decision is made on whether or not to include `named()` method]

### Element by ID

    func ID(_ elementID: Any) -> PREFIXItem

Examples:

    textedit.windows.ID(4321)

Syntax:

    elements.ID(selector)
            selector : Any -- the object's id (as defined in its 'id' property)

### Element by absolute position

    var first: PREFIXItem {get}
    var middle: PREFIXItem {get}
    var last: PREFIXItem {get}
    var any: PREFIXItem {get}

Examples:

    textedit.documents.first.text.paragraphs.last
    finder.desktop.files.any

Syntax:

    elements.first -- first element
    elements.middle -- middle element
    elements.last -- last element
    elements.any -- random element

### Element by relative position

    func previous(_ elementClass: Symbol? = nil) -> PREFIXItem
    func next(_ elementClass: Symbol? = nil) -> PREFIXItem

Examples:

    textedit.documents[1].characters[3].next()
    textedit.documents[1].paragraphs[-1].previous(TED.word)

Syntax:

    // nearest element of a given class to appear before the specified element:
    element.previous(elementClass)

    // nearest element of a given class to appear after the specified element
    element.next(elementClass)

            elementClass : Symbol -- the name of the previous/next element's class;
                                     if omitted, the current element's class is used

### Elements by range

    subscript(from: Any, to: Any) -> PREFIXItems

Examples:

    textedit.documents[1, 3]
    finder.home.folders["Documents", "Movies"]
    texeditplus.documents[1].text[TEPCon.characters[5], TEPCon.words[-2]]

Caution:

By-range specifiers must be constructed as `elements[start,end]`, not `elements[start...end]`, as `Range<T>` types are not supported.

Syntax:

    elements[start, end]
            start : Int | String | PREFIXItem -- start of range
            end : Int | String | PREFIXItem -- end of range

Range references select all elements between and including two object specifiers indicating the start and end of the range. The start and end specifiers are normally declared relative to the container of the elements being selected.

These sub-specifiers are constructed using the glue's `PREFIXCon` constant, e.g. `TEDCon`, as their root. For example, to indicate the third paragraph relative to the currrent container object:

    TEDCon.paragraphs[3]

Thus, to specify all paragraphs from paragraph 3 to paragraph -1:

    paragraphs[TEDCon.paragraphs[3], TEDCon.paragraphs[-1]]

For convenience, sub-specifiers can be written in shorthand form where their element class is the same as the elements being selected; thus the above can be written more concisely as:

    paragraphs[3, -1]

Some applications can handle more complex range references. For example, the following will work in Tex-Edit Plus:

    words[TEPCon.characters[5], TEPCon.paragraphs[-2]]

### Elements by test

    subscript(test: TestClause) -> PREFIXItems

Examples:

    textedit.documents[TEDIts.path == MissingValue]

    finder.desktop.files[FINIts.nameExtension.isIn(["txt", "rtf"])
                         && FINIts.modificationDate > (Date()-60*60*24*7)]

Syntax:

A specifier to each element that satisfies one or more conditions specified by a test specifier:

    elements[selector]
            selector : PREFIXSpecifier -- test specifier

Test expressions consist of the following:

- A test specifier relative to each element being tested. This specifier must be constructed using the glue's 'PREFIXIts' root, e.g. `TEDIts`. Its-based references support all valid reference forms, allowing you to construct references to its properties and elements. For example:

  TEDIts TEDIts.size TEDIts.words.first

- One or more conditional/containment tests, implemented as operators/methods on the specifier being tested. The left-hand operand/receiver must be a `PREFIXSpecifier` instance. The right-hand operand/argument can be any value; its type is always `Any`.

  Syntax:

      specifier < value
      specifier <= value
      specifier == value
      specifier != value
      specifier > value
      specifier >= value
      specifier.beginsWith(value)
      specifier.endsWith(value)
      specifier.contains(value)
      specifier.isIn(value)

  Examples:

      TEDIts == ""
      FINits.size > 1024
      TEDIts.words.first.beginsWith("A")
      TEDIts.characters.first == TEDIts.characters.last

  Caution: if assigning a test specifier to a variable, the variable must be explicitly typed to ensure the compiler uses the correct operator overload, e.g.: [TO DO: this sort of thing should be discouraged in practice; at most, it should be a footnote re. `==` overloading quirk]

      let test: TEDSpecifier = TEDIts.color == [0,0,0]
      let query = textedit.documents[1].words[test]

- Zero or more logical tests, implemented as properties/methods on conditional tests. All operands must be conditional/containment and/or logic test specifiers.

  Syntax:

      test && test
      test || test
      !test

  Examples:

      !(TEDIts.contains("?"))

      FINIts.size > 1024 && FINIts.size < 10240

      TEDIts.words[1].beginsWith("A") || TEDIts.words[1].contains("ce") || TEDIts.words[2] == "foo"

### Element insertion location

Insertion locations can be specified at the beginning or end of all elements, or before or after a specified element or element range.

    var beginning: PREFIXSpecifier
    var end: PREFIXSpecifier
    var before: PREFIXSpecifier
    var after: PREFIXSpecifier

Examples:

    textedit.documents.end
    textdit.documents[1].paragraphs[-1].before

Syntax:

    elements.beginning
    elements.end
    element.before
    element.after