i  Poe:   interactive poetry editor
1 Using an i  Poe Language
1.1 A Guided Tour
1.2 i  Poe Concepts
1.2.1 Built-in Poetic Forms
1.3 Pragmatics
1.3.1 Connecting to an ipoe Database
1.3.2 Configuration
2 Defining a new i  Poe Language
2.1 Installing a Language
2.1.1 The Fast Way
2.1.2 The Franchised Way
2.1.3 The Directory-Free Way
2.2 Example Languages
3 i  Poe API
3.1 English-Language Tools
infer-rhyme=?
infer-syllables
integer->word*
integer->word/  space
suggest-spelling
3.2 Poetic Form API
poem?
stanza?
line?
word?
stanza
line
word
line=?
word=?
line->word*
poem->stanza*
poem->word*
stanza->line*
poem-count-stanza*
stanza-count-line*
last-word
last-stanza
contains-word?
*current-poem*
3.3 Web Scraping
scrape-word
word-result?
word-scraper?
make-word-scraper
dictionary.com
american-heritage
merriam-webster
the-free-dictionary
scrape-rhyme
rhyme-result?
almost-rhymes?
rhymes?
url->sxml
sxml-200?
id?
class?
contains-text?
scrape-logger
4 External Links
6.12

iPoe: interactive poetry editor

 #lang ipoe package: ipoe
The ipoe package is an extensible compiler for poetry.

You can use ipoe to:

Each poetic form is implemented as a Racket #lang. The syntax of these languages is plain text; their semantics is to check the text for spelling, grammar, and other errors.

NOTE: this package is unstable. The purpose of this documentation is just to describe how ipoe fits together as of January 2017.

1 Using an iPoe Language

1.1 A Guided Tour

Let’s write a haiku. According to Wikipedia:

A typical haiku is a three-line observation about a fleeting moment involving nature. .... with 17 syllables arranged in a 5–7–5 pattern.

Here’s a first shot:

    #lang ipoe/haiku

    

    pit pat pit pat, rain?

    or clicking on a keyboard?

    it's 3 am I don't care no more

Save the file as ./first.haiku and run raco make first.haiku to compile the poem. Compiling will check the text of the poem against the ipoe/haiku poetic form.

If this is your first time using ipoe, you will see the following message:

    Missing run-time parameter for database username. Please enter a username to connect to the ipoe database.

    Enter your database username (or #f to skip):

    ipoe>

Just type #f and skip for now. (If you’re feeling brave, try running raco ipoe init or browsing Pragmatics.) The next thing you will see is:

    Starting ipoe without a database connection (in online-only mode)

    Word 1 on line 1 of stanza 0 ('clicking') is undefined. Consider adding it to the dictionary.

    Word 4 on line 2 of stanza 0 ('dont') is undefined. Consider adding it to the dictionary.

    ipoe: Expected 7 syllables on Line 1 of Stanza 0, got 5 syllables

      compilation context...:

         /Users/ben/code/racket/my-pkgs/ipoe/ipoe/first.rkt

Okay! This means ipoe checked our poem and found a problem: the second line has 5 syllables but was expected to have 7 syllables. The trouble is that ipoe couldn’t figure out how many syllables are in the word “clicking” (by asking the internet, see the ipoe/scrape module).

We can’t add “clicking” to the dictionary because we haven’t set up a database. But we can bypass the issue by claiming our poetic license. Change the poem to read:

    #lang ipoe/haiku

    #:poetic-license 10

    

    pit pat pit pat, rain?

    or clicking on a keyboard?

    it's 3 am I don't care no more

Now compile the poem; you should see:

    Missing run-time parameter for database username. Please enter a username to connect to the ipoe database.

    Enter your database username (or #f to skip):

    ipoe> #f

    Starting ipoe without a database connection (in online-only mode)

    Word 1 on line 1 of stanza 0 ('clicking') is undefined. Consider adding it to the dictionary.

    Word 4 on line 2 of stanza 0 ('dont') is undefined. Consider adding it to the dictionary.

    Finished checking poem.

    - Misspelled word 'clicking'; position 1 on line 1 of stanza 0.

    - Misspelled word 'dont'; position 4 on line 2 of stanza 0. Maybe you meant 'on'?

    - Expected 7 syllables on Line 1 of Stanza 0, got 5 syllables

    - Expected 5 syllables on Line 2 of Stanza 0, got 7 syllables

    Remaining poetic license: 6

Success! And now you’ve seen the basics of ipoe:
  • The #lang line declares a poetic form. Poetic forms can specify the number of lines, number of stanzas, rhyme scheme, and number of syllables in the following text. See Defining a new iPoe Language for more details.

  • ipoe checks the internet to get the spelling, number of syllables, and rhymes for a given word. If you have an ipoe database, it stores this information. Otherwise, the data for words is cached locally (in ./compiled/ipoe.cache).

  • Poems can start with a few keyword/value pairs to alter the poem-checker.

1.2 iPoe Concepts

An iPoe poem is a text file with #lang <spec> as its first line, where <spec> refers to an iPoe poetic form.

A poetic form is a grammar; it specifies how a class of poems should look and sound.

A rhyme scheme is an important part of a poetic form; it declares the number of stanzas, number of lines in each stanza, number of syllables in each line, and constrains certain lines to rhyme with one another.

A iPoe stanza is a group of lines. Different stanzas are separated by at least 2 consecutive newline characters.

A iPoe line is a sequence of words. Different lines are separated by a single newline character.

A iPoe word is basically a sequence of non-whitespace characters. Different words are separated by at least one whitespace character. (This definition is intentionally vague.)

syllables are “units of pronunciation” in a word (thanks Google).

Two words rhyme if they end with the same sound.

Two words almost rhyme if they kind-of-but-not-really end with the same sound.

The rules of a poetic form are more like guidelines. Good poetry doesn’t always follow the rules, and poets have a poetic license to break the rules.

How much license? I don’t know. How much does it cost to break a rule? I don’t know, but I tried to set good defaults. Have fun with this until we have something better.

An iPoe database stores known words, their properties, and the relation between them. Once you have an iPoe database, it will update itself. But it tends to ask for too much help, you can disable the help by putting #:interactive? #f at the top of your poems or configuration file.

1.2.1 Built-in Poetic Forms

 #lang ipoe/cinquain package: ipoe
 #lang ipoe/clerihew package: ipoe
 #lang ipoe/couplet package: ipoe
 #lang ipoe/haiku package: ipoe
 #lang ipoe/limerick package: ipoe
 #lang ipoe/rondelet package: ipoe
 #lang ipoe/sestina package: ipoe
 #lang ipoe/sextilla package: ipoe
 #lang ipoe/tanaga package: ipoe
 #lang ipoe/tercet package: ipoe

1.3 Pragmatics

1.3.1 Connecting to an ipoe Database

If you install PostgreSQL and start a server, running raco ipoe init should start an iPoe database.

1.3.2 Configuration

Global configuration file: (writable-config-file ".ipoe" #:program "ipoe")

Local configuration file: "./.ipoe"

There are many configuration options. Put these in a configuration file or at the top of a poem. Global configurations have the least precedence, in-file configurations have the most.

2 Defining a new iPoe Language

The ipoe language is a specification language for poetic forms.

The syntax accepted by the #lang ipoe reader is keyword-based:

 

stmt

 ::= 

#:name identifier

 

  |  

#:description string?

 

  |  

#:syllables exact-positive-integer?

 

  |  

#:rhyme-scheme rhyme-scheme

 

  |  

#:constraint constraint-expr

 

rhyme-scheme

 ::= 

( stanza-scheme* )

 

stanza-scheme

 ::= 

( line-scheme* )

 

line-scheme

 ::= 

( rhyme . syllable )

 

  |  

syllable

 

  |  

rhyme

 

rhyme

 ::= 

symbol?

 

  |  

wildcard

 

syllable

 ::= 

exact-positive-integer?

 

  |  

wildcard

 

wildcard

 ::= 

*

 

constraint-expr

 ::= 

expr

In summary, a #lang ipoe program is a sequence of statements.
  • The #:name statement must appear once. This statement declares the name of the poetic form (currently only used for debugging).

  • The #:description statement may appear at most once; it gives a brief description of the poetic form (for debugging).

  • The #:syllables statement may appear at most once. When #:syllables N is given, any wildcard syllables in the rhyme-scheme (implicit or explicit) are replaced with N.

  • The #:rhyme-scheme statement may appear at most once. A rhyme-scheme specifies:
    • the number of stanzas in the poetic form,

    • the number of lines in each stanza,

    • the number of syllables in each line, and

    • which lines must rhyme with one another.

    An empty rhyme scheme allows any number of stanzas. A line-scheme that omits the rhyme or syllable component has an implicit wildcard in that component. A rhyme can be any symbol; lines that must rhyme must have equal symbols (in the sense of eq?).

  • The #:constraint statement may appear any number of times. Each statement may contain arbitrary expressions using (most!) identifiers from the racket/base and ipoe/poetic-form namespaces.

NOTE: The syntax of constraint-expr nonterminals is bad IMO. Ideally it should be a simple, Racket-like domain-specific-language. Allowing anything in racket/base is probably too much power, but ipoe/poetic-form by itself isn’t enough to express the constraints in certain poems (ipoe/sestina).

2.1 Installing a Language

2.1.1 The Fast Way

Suppose you have a #lang ipoe program with name my-form. The easiest way to start writing my-form poems is:
  1. Install the gnal-lang package.

  2. Create a directory my-form/ and a sub-directory my-form/lang/.

  3. Save your program as my-form/lang/reader.rkt.

Now you can write programs using the new poetic form:

    #lang gnal "my-form"

    

    Racket is fun because #:fun is a keyword!

Compiling the program will check the text against the my-form specification.

2.1.2 The Franchised Way

A longer but prettier method is:
  1. Create the my-form/lang/ directory and save the ipoe program as my-form/lang/reader.rkt.

  2. Run raco pkg install ./my-form.

  3. Write programs that start with #lang my-form.

2.1.3 The Directory-Free Way

TODO should be possible to defined a reader submodule, but this doesn’t work right now!

2.2 Example Languages

Browse the source code for more examples.

#lang ipoe/haiku
#lang ipoe
 
#:name haiku
#:rhyme-scheme {[5 7 5]}
#:description
"1 stanza, 3 lines
No rhyme constraints
5 syllables in first line
7 in second
5 in last"

#lang ipoe/couplet
#lang ipoe
 
#:name couplet
#:rhyme-scheme {[A A]}

#lang ipoe/english-sonnet
#lang ipoe
 
#:name english-sonnet
#:syllables 10
#:rhyme-scheme {[A B A B]
                [C D C D]
                [E F E F]
                [G G]}

#lang ipoe/villanelle
#lang ipoe
 
#:name villanelle
#:rhyme-scheme {[R1 B R2]
                [A  B R1]
                [A  B R2]
                [A  B R1]
                [A  B R2]
                [A  B R1 R2]}
#:syllables 10
#:constraint
  (line=? (line 0 (stanza 0))
          (line 2 (stanza 1))
          (line 2 (stanza 3))
          (line 2 (stanza 5)))
#:constraint
  (line=? (line 2 (stanza 0))
          (line 2 (stanza 2))
          (line 2 (stanza 4))
          (line 3 (stanza 5)))

3 iPoe API

The following modules have been useful internally to ipoe and are stable enough to be used in (or ported to) other projects. Hopefully, these will eventually re-appear in a more general context (scribble, sxml, pkgs.racket-lang.org, etc.).

3.1 English-Language Tools

 (require ipoe/english) package: ipoe
The ipoe/english module provides simple tools for working with the English language.

procedure

(infer-rhyme=? str1 str2)  boolean?

  str1 : string?
  str2 : string?
Guess whether the given words rhyme with one another.

Examples:
> (infer-rhyme=? "cat" "hat")

#t

> (infer-rhyme=? "sauce" "boss")

#f

> (infer-rhyme=? "books" "carrots")

#t

procedure

(infer-syllables str)  exact-nonnegative-integer?

  str : string?
Guess the number of syllables in the given word.

Technically, the number of syllables in some words depends on their pronunciation (does “flower” have 1 or 2 syllables?), but this function just returns a natural number without thinking very hard.

Examples:
> (infer-syllables "month")

1

> (infer-syllables "hour")

2

> (infer-syllables "munificent")

4

procedure

(integer->word* i)  (listof string?)

  i : exact-integer?
Converts an integer to a list of English words. Raises an exception (specifically, an exn:fail:contract?) if (abs i) is greater than an arbitrary (but pretty big) limit.

Examples:
> (integer->word* 2)

'("two")

> (integer->word* -21)

'("negative" "twenty" "one")

procedure

(integer->word/space i)  string?

  i : exact-integer?
Returns the same result as:

(string-join (integer->word* i) " ")

procedure

(suggest-spelling str    
  [#:max-distance d    
  #:limit n])  (listof string?)
  str : string?
  d : exact-nonnegative-integer? = 2
  n : exact-nonnegative-integer? = 10
Return a list of common English words with Levenshtein distance (see the levenshtein package) at most d from the given string. The result list has no more than n elements.

Examples:
> (suggest-spelling "tpyo")

'("to" "two" "try" "too" "toy" "pro" "spy")

> (suggest-spelling "balon" #:limit 3)

'("along" "alone" "below")

3.2 Poetic Form API

The ipoe/poetic-form module provides functions over the internal representation of an iPoe poem.

procedure

(poem? x)  boolean?

  x : any/c
A predicate for iPoe poems.

procedure

(stanza? x)  boolean?

  x : any/c
A predicate for iPoe stanzas.

procedure

(line? x)  boolean?

  x : any/c
A predicate for iPoe lines.

procedure

(word? x)  boolean?

  x : any/c
A predicate for iPoe words.

procedure

(stanza i [p])  stanza?

  i : integer?
  p : poem? = (*current-poem*)
Returns the stanza in poem p at position i, where the first stanza is at position 0.

procedure

(line i st)  line?

  i : exact-nonnegative-integer?
  st : stanza?
Returns the line in stanza st at position i.

procedure

(word i ln)  word?

  i : exact-nonnegative-integer?
  ln : line?
Returns the word in line ln at position i.

procedure

(line=? ln1 ln2)  boolean?

  ln1 : line?
  ln2 : line?
Returns #true if the given lines contain the same words.

procedure

(word=? w1 w2)  boolean?

  w1 : word?
  w2 : word?
Return #true if the given words are equal.

procedure

(line->word* ln)  (sequence/c word?)

  ln : line?
Splits a line into words.

procedure

(poem->stanza* p)  (sequence/c stanza?)

  p : poem?
Splits a poem into stanzas.

procedure

(poem->word* p)  (sequence/c word?)

  p : poem?
Return a sequence of all words in the given poem.

procedure

(stanza->line* st)  (sequence/c line?)

  st : stanza?
Return a sequence of lines in the given stanza.

Count the number of stanzas in the given poem.

Count the number of lines in the given stanza.

procedure

(last-word ln)  word?

  ln : line?
Returns the last word on the given line.

procedure

(last-stanza [poem])  stanza?

  poem : poem? = *current-poem*
Returns the last stanza of the given poem.

procedure

(contains-word? ln w)  boolean?

  ln : line?
  w : word?
Returns #true if the given line contains the given word.

parameter

(*current-poem*)  (or/c poem? #f)

(*current-poem* p)  void?
  p : (or/c poem? #f)
 = #f
Poetic forms defined using #lang ipoe can assume this parameter holds the poem that is currently being checked.

3.3 Web Scraping

 (require ipoe/scrape) package: ipoe
The ipoe/scrape module provides tools for scraping word and rhyme data from the internet.

procedure

(scrape-word str)  (or/c word-result? #f)

  str : string?
Search the internet for information about the given word.

procedure

(word-result? x)  boolean?

  x : any/c
Predicate for the results of a successful word scrape.

procedure

(word-scraper? x)  boolean?

  x : any/c
Predicate for a word scraper.

procedure

(make-word-scraper sym 
  #:word->url word->url 
  #:sxml->definition sxml->defn 
  #:sxml->num-syllables sxml->syll 
  #:sxml->word sxml->word) 
  (-> string? (or/c word-result? #f))
  sym : symbol?
  word->url : (-> string? string?)
  sxml->defn : (-> sxml (or/c string? #f))
  sxml->syll : (-> sxml (or/c exact-nonnegative-integer? #f))
  sxml->word : (-> sxml (or/c string? #f))
Builds a new word scraper, that is, an opaque structure that scrapes a fixed web resource for information on words.
  • sym briefly describes the resource being scraped.

  • word->url converts a word to a URL for a web page that may have information about the word.

  • sxml->defn parses an sxml page for the definition of a word.

  • sxml->syll parses an sxml page for the number of syllables in a word.

  • sxml->word parses an sxml page for the word the page describes.

If any of the above procedures fail, they should return #false. When any one procedure returns #false during a call (scraper w), then the overall result is #false.

value

dictionary.com : word-scraper?

value

american-heritage : word-scraper?

value

merriam-webster : word-scraper?

value

the-free-dictionary : word-scraper?

Built-in word scrapers.

procedure

(scrape-rhyme str)  rhyme-result?

  str : string?
Search the internet for words that rhyme and almost rhyme with the given word.

procedure

(rhyme-result? x)  boolean?

  x : any/c
Prefab structure representing the results of scraping for rhymes.

procedure

(almost-rhymes? rr str)  boolean?

  rr : rhyme-result?
  str : string?
Return #true if str is listed in rr as an “almost rhyme”.

procedure

(rhymes? rr str)  boolean?

  rr : rhyme-result?
  str : string?
Return #true if str is listed in rr as a rhyming word.

procedure

(url->sxml url)  sxml:top?

  url : (or/c url? string?)
Send a GET request to the given url and parse the result to SXML. Return the parsed response.

procedure

(sxml-200? sx)  boolean?

  sx : sxml:top?
Returns #true if the given sxml object represents a response to a successful HTML request. Use this on the result of url->sxml to see if the request was successful.

procedure

(id? id-str)  (-> (listof sxml) any/c (listof sxml))

  id-str : string?
Returns a procedure that filters sxml elements, keeping only those elements with an id attribute with value id-str.

I do not know the purpose of the second argument to the procedure returned by id?.

Example:
> ((sxpath `(// div ,(id? "foo") *text*))
   '(div
      (div (@ id "foo") "foo-text")
      (div (@ id "bar") "bar-text")))

'()

procedure

(class? class-str [<?])  (-> (listof sxml) any/c (listof sxml))

  class-str : string?
  <? : (-> string? string? boolean?) = string=?
Similar to id?, but filters based on the class attribute. Override the <? argument to treat class-str as a substring in the value of sxml elements’ class attribute.

Example:
> ((sxpath `(// div ,(class? "a" string-prefix?) *text*))
   '(div
      (div (@ class "abc") "success")
      (div (@ class "def") "failure")))

'()

procedure

(contains-text? pattern)

  (-> (listof sxml) any/c (listof sxml))
  pattern : regexp?
Similar to id?, but filters sxml elements to keep only those whose *text* contents match the given pattern (in the sense of regexp-match?).

value

scrape-logger : logger?

A logger that receives iPoe scraping events, such as rejected GET requests.

4 External Links