/*************************************************************************/
/* Copyright (c) 2004 */
/* Daniel Sleator, David Temperley, and John Lafferty */
/* Copyright 2013, 2014 Linas Vepstas */
/* All rights reserved */
/* */
/* Use of the link grammar parsing system is subject to the terms of the */
/* license set forth in the LICENSE file included with this software. */
/* This license allows free redistribution and use in source and binary */
/* forms, with or without modification, subject to certain conditions. */
/* */
/*************************************************************************/
#include "api-structures.h"
#include "connectors.h"
#include "dict-common/dict-utils.h" // size_of_expression
#include "disjunct-utils.h"
#include "linkage/linkage.h"
#include "post-process/post-process.h" // compute_domain_names
#include "print.h"
#include "tokenize/word-structures.h" // Word_struct
#include "wcwidth.h"
#define LEFT_WALL_SUPPRESS ("Wd") /* If this connector is used on the wall, */
/* then suppress the display of the wall. */
#define RIGHT_WALL_SUPPRESS ("RW")/* If this connector is used on the wall, */
/* then suppress the display of the wall. */
#define HEAD_CHR ('h') /* Single char marking head-word. */
#define DEPT_CHR ('d') /* Single char marking dependent word */
/**
* Find the position, measured in column-widths, of the center of
* each word. Also find the offset of each word, relative to the
* previous one, needed to fully fit the names of the links between
* them. The offset is again measured in column-widths, not in bytes!
*
* By "column widths", it is meant the number of terminal columns
* needed to display a glyph. Almost all indoeuropean glyphs are one
* column wide. Almost all CJK glyphs (hanzi, kanji) are two columns
* wide. These widths are NOT to be confused with UTF-8 byte size.
*
* FIXME Long link names between more distant words may still not
* fit the space between these words.
*
* Return the number of bytes needed for all the words, including
* the space needed for the link names as described above. Note that
* this byte length might be less than the glyph width! e.g. the
* ASCII chars in the range 01 to 1F usually print in two columns,
* but require only one byte to encode.
*/
static size_t
set_centers(const Linkage linkage, int center[], int word_offset[],
bool print_word_0, int N_words_to_print)
{
int i, tot;
size_t n;
int start_word = print_word_0 ? 0 : 1;
int *link_len = alloca(linkage->num_words * sizeof(*link_len));
size_t max_bytes_in_line = 0; /* Needed picture array line length */
memset(link_len, 0, linkage->num_words * sizeof(*link_len));
for (n = 0; n < linkage->num_links; n++)
{
Link *l = &linkage->link_array[n];
if (l->lw + 1 == l->rw)
{
link_len[l->rw] = strlen(l->link_name) +
(DEPT_CHR == connector_string(l->rc)[0]) +
(HEAD_CHR == connector_string(l->rc)[0]) +
(DEPT_CHR == connector_string(l->lc)[0]) +
(HEAD_CHR == connector_string(l->lc)[0]);
}
}
tot = 0;
for (i = start_word; i < N_words_to_print; i++)
{
int len, middle;
/* Centers obtained by counting the characters column widths,
* not the bytes in the string. */
len = utf8_strwidth(linkage->word[i]);
middle = tot + (len/2);
#if 1 /* Long labels - disable in order to compare output with old versions. */
if (i > start_word)
center[i] = MAX(middle, center[i-1] + link_len[i] + 1);
else
#endif
center[i] = middle;
word_offset[i] = center[i] - middle;
tot += len+1 + word_offset[i];
// We use 2x strlen, because invalid UTF-8 chars get padding.
// But we don't know how much padding, right now. In general,
// this will sharply over-count, but is needed to deal with
// garbage data that is attempting to crash/buffer-overflow.
max_bytes_in_line += word_offset[i] + 2*strlen(linkage->word[i]) + 1;
}
return max_bytes_in_line;
}
/* The following are all for generating postscript */
typedef struct
{
int N_rows; /* N_rows -- the number of rows */
/* tells the height of the links above the sentence */
int * link_heights;
/* the word beginning each row of the display */
int * row_starts;
} ps_ctxt_t;
/**
* Prints s then prints the last |t|-|s| characters of t.
* If s is longer than t, it truncates s.
* Handles utf8 strings correctly.
* Assumes that the characters of t are all blanks,
* so that column-width of t == num bytes in t == num chars in t.
*/
static void left_append_string(dyn_str * string, const char * s, const char * t)
{
/* These are column-widths. */
size_t twidth = strlen(t);
size_t swidth = utf8_strwidth(s);
for (size_t i = 0; i < twidth; )
{
if (i < swidth && *s != '\0')
{
// The width might be negative, if the value is not a valid
// UTF-8 character. Assume that it will be printed with a
// two-column-wide "box font".
int sw = utf8_charwidth(s);
if (sw < 0) sw = 2;
i += sw;
t += sw;
// Meanwhile, s advances by bytes, and not by column widths.
s += append_utf8_char(string, s);
}
else
{
append_utf8_char(string, t);
i++;
t++;
}
}
}
static void print_a_link(dyn_str * s, const Linkage linkage, LinkIdx link)
{
WordIdx l, r;
const char *label, *llabel, *rlabel;
l = linkage_get_link_lword(linkage, link);
r = linkage_get_link_rword(linkage, link);
label = linkage_get_link_label(linkage, link);
llabel = linkage_get_link_llabel(linkage, link);
rlabel = linkage_get_link_rlabel(linkage, link);
if (l == 0)
{
left_append_string(s, LEFT_WALL_DISPLAY, " ");
}
else if (l == (linkage_get_num_words(linkage) - 1))
{
left_append_string(s, RIGHT_WALL_DISPLAY, " ");
}
else
{
left_append_string(s, linkage_get_word(linkage, l), " ");
}
/* XXX FIXME -- the space allocated to a link name should depend
* on the longest link-name in the sentence! */
left_append_string(s, llabel, " ");
if (DEPT_CHR == llabel[0])
dyn_strcat(s, " <---");
else if (HEAD_CHR == llabel[0])
dyn_strcat(s, " >---");
else
dyn_strcat(s, " ----");
left_append_string(s, label, "-----");
if (DEPT_CHR == rlabel[0])
dyn_strcat(s, "-> ");
else if (HEAD_CHR == rlabel[0])
dyn_strcat(s, "-< ");
else
dyn_strcat(s, "-- ");
left_append_string(s, rlabel, " ");
append_string(s, " %s\n", linkage_get_word(linkage, r));
}
/**
* To the left of each link, print the sequence of domains it is in.
* Printing a domain means printing its type.
* Takes info from pp_link_array and pp and chosen_words.
*/
char * linkage_print_links_and_domains(const Linkage linkage)
{
int link, longest, j;
int N_links = linkage_get_num_links(linkage);
dyn_str * s = dyn_str_new();
const char ** dname;
compute_domain_names(linkage);
longest = 0;
for (link=0; link<N_links; link++)
{
assert (linkage_get_link_lword(linkage, link) < SIZE_MAX, "Missing word in link");
if (linkage_get_link_num_domains(linkage, link) > longest)
longest = linkage_get_link_num_domains(linkage, link);
}
for (link=0; link<N_links; link++)
{
assert (linkage_get_link_lword(linkage, link) < SIZE_MAX, "Missing word in link");
dname = linkage_get_link_domain_names(linkage, link);
for (j=0; j<linkage_get_link_num_domains(linkage, link); ++j) {
append_string(s, " (%s)", dname[j]);
}
for (; j<longest; j++) {
dyn_strcat(s, " ");
}
dyn_strcat(s, " ");
print_a_link(s, linkage, link);
}
dyn_strcat(s, "\n");
if (linkage_get_violation_name(linkage) != NULL) {
dyn_strcat(s, "P.P. violations:\n");
append_string(s, " %s\n\n", linkage_get_violation_name(linkage));
}
return dyn_str_take(s);
}
char * linkage_print_disjuncts(const Linkage linkage)
{
const char * dj;
int w;
dyn_str * s = dyn_str_new();
int nwords = linkage->num_words;
/* Loop over each word in the sentence */
for (w = 0; w < nwords; w++)
{
int pad = 21;
float cost;
const char *infword;
Disjunct *disj = linkage->chosen_disjuncts[w];
if (NULL == disj) continue;
/* Subscript mark will be cleaned up by append_string(). */
// infword = disj->word_string;
/* ... except that subscript marks confuse utf8_strwidth() */
/* linkage->word[w] should be same as disj->word_string except
* that the subscript mark has been replaced by a dot. */
infword = linkage->word[w];
/* Make sure the glyphs align during printing. */
pad += strlen(infword) - utf8_strwidth(infword);
dj = linkage_get_disjunct_str(linkage, w);
if (NULL == dj) dj = "";
cost = (float)linkage_get_disjunct_cost(linkage, w);
append_string(s, "%*s %6s %s\n",
pad, infword, cost_stringify(cost), dj);
}
return dyn_str_take(s);
}
/**
* Postscript printing ...
* FIXME:
* 1. It is invoked after a call to linkage_print_diagram_ctxt() with a
* screen width of 8000. But it actually cannot handle a screen width
* greater than a page-width since it doesn't know to fold without the
* help of the row_starts array which tells it on which word each
* folded line starts (a garbled printout results).
* 2. It cannot handle utf-8 (garbage is printed).
* 3. Due to the added ability of folding long words, the row_starts
* array is not sufficient for telling where to start the next line
* (but this doesn't matter for now and can be fixed along with
* problem no. 1 above).
*/
static char *
build_linkage_postscript_string(const Linkage linkage,
bool display_walls, ps_ctxt_t *pctx)
{
bool print_word_0 = true, print_word_N = true;
int N_links = linkage->num_links;
Link *ppla = linkage->link_array;
/* Do we want to print the left and right walls? */
if (!display_walls)
{
int N_wall_connectors = 0;
for (int j=0; j<N_links; j++)
{
if (0 == ppla[j].lw)
{
if (ppla[j].rw == linkage->num_words-1) continue;
if (easy_match(connector_string(ppla[j].lc), LEFT_WALL_SUPPRESS))
print_word_0 = false;
if (++N_wall_connectors > 1)
{
print_word_0 = true;
break;
}
}
}
N_wall_connectors = 0;
for (int j=0; j<N_links; j++)
{
if (ppla[j].rw == linkage->num_words-1)
{
if (easy_match(connector_string(ppla[j].lc), RIGHT_WALL_SUPPRESS))
print_word_N = false;
if (++N_wall_connectors > 1)
{
print_word_N = true;
break;
}
}
}
}
int N_words_to_print = linkage->num_words;
if (!print_word_N) N_words_to_print--;
/* The postscriipt string */
dyn_str * string = dyn_str_new();
dyn_strcat(string, "[");
int doff = print_word_0 ? 0 : 1;
for (int j=doff; j<N_words_to_print; j++)
{
if ((j%10 == 0) && (j>doff)) dyn_strcat(string, "\n");
append_string(string, "(%s)", linkage->word[j]);
}
dyn_strcat(string,"]");
dyn_strcat(string,"\n");
dyn_strcat(string,"[");
int k = 0;
for (int link=0; link<N_links; link++) {
if (!print_word_0 && (ppla[link].lw == 0)) continue;
if (!print_word_N && (ppla[link].rw == linkage->num_words-1)) continue;
if ((k%7 == 0) && (k>0)) dyn_strcat(string,"\n");
k++;
append_string(string,"[%d %d %d",
ppla[link].lw - doff, ppla[link].rw - doff,
pctx->link_heights[link]);
append_string(string," (%s)]", ppla[link].link_name);
}
dyn_strcat(string,"]");
dyn_strcat(string,"\n");
dyn_strcat(string,"[");
for (int j=0; j < pctx->N_rows; j++)
{
if (j>0) append_string(string, " %d", pctx->row_starts[j]);
else append_string(string,"%d", pctx->row_starts[j]);
}
dyn_strcat(string,"]\n");
return dyn_str_take(string);
}
#define HEIGHT_INC 10
static void diagram_alloc_tmpmem(size_t **start, char ***pic, char ***xpic,
size_t *cur_height, size_t max_height,
size_t max_bytes, size_t num_cols)
{
assert(num_cols <= max_bytes, "Columns overflow");
assert(max_height > *cur_height, "New diagram height is too small");
*start = realloc(*start, max_height * sizeof(size_t));
*pic = realloc(*pic, max_height * sizeof(char *));
*xpic = realloc(*xpic, max_height * sizeof(char *));
for (size_t i = *cur_height; i < max_height; i++)
{
/* Allocate memory for both pic and xpic. */
char *picmem = malloc(max_bytes * 2 + 2);
(*pic)[i] = picmem;
(*xpic)[i] = picmem + max_bytes;
memset((*pic)[i], ' ', num_cols);
(*pic)[i][num_cols] = '\0';
}
*cur_height = max_height;
}
typedef struct
{
Link *lnk;
unsigned int len; /* Length of this link */
} link_by_length;
static int by_link_len(const void *a, const void *b)
{
const link_by_length * const *la = a;
const link_by_length * const *lb = b;
return (*la)->len - (*lb)->len;
}
static void sort_link_lengths(Link *ppla, link_by_length *ll,
unsigned int N_links)
{
link_by_length *ll_tmp = alloca(N_links * sizeof(*ll_tmp));
link_by_length **lla = alloca(N_links * sizeof(*lla));
for (unsigned int j = 0; j < N_links; j++)
{
Link *lnk = &ppla[j];
assert(lnk->link_name != NULL, "Missing link name");
lla[j] = &ll_tmp[j];
ll_tmp[j].lnk = lnk;
ll_tmp[j].len = (unsigned int)(lnk->rw - lnk->lw);
}
qsort(lla, N_links, sizeof(*lla), by_link_len);
/* Return the sort result through the ll parameter. */
for (unsigned int j = 0; j < N_links; j++)
ll[j] = *lla[j];
}
static void diagram_free_tmpmem(size_t *start, char **pic, char **xpic,
size_t cur_height)
{
for (size_t i = 0; i < cur_height; i++)
free(pic[i]);
free(start);
free(pic);
free(xpic);
}
/**
* Print the indicated linkage into a utf8-diagram.
* Works fine for general utf8 multi-byte sentences.
* Links and connectors are still mostly assumed to be ASCII, though;
* to fix this, grep for "strlen" in the code below, replace by utf8 len.
*
* Returned string is allocated with exalloc.
* Needs to be freed with linkage_free_diagram()
*/
static char *
linkage_print_diagram_ctxt(const Linkage linkage,
bool display_walls,
size_t x_screen_width,
ps_ctxt_t *pctx)
{
if (x_screen_width <= 2)
{
prt_error("Error: Cannot print diagram: "
"Screen width %zu is too narrow.\n", x_screen_width);
return NULL;
}
bool display_short = true;
unsigned int i, j, k, cl, cr, inc, row, top_row, top_row_p1;
const char *s;
char *t;
bool print_word_0 = true, print_word_N = true;
int *center = alloca((linkage->num_words+1)*sizeof(int));
int *word_offset = alloca((linkage->num_words+1) * sizeof(*word_offset));
unsigned int N_links = linkage->num_links;
Link *ppla = linkage->link_array;
dyn_str * string;
unsigned int N_words_to_print;
// Avoid a pathological case and the resulting crash.
if (0 == linkage->num_words) return strdup("");
link_by_length *ll = alloca(N_links * sizeof(*ll));
sort_link_lengths(ppla, ll, N_links);
string = dyn_str_new();
/* Do we want to print the left and right walls? */
if (!display_walls)
{
int N_wall_connectors = 0;
for (j=0; j<N_links; j++)
{
if (0 == ppla[j].lw)
{
if (ppla[j].rw == linkage->num_words-1) continue;
if (easy_match(connector_string(ppla[j].lc), LEFT_WALL_SUPPRESS))
print_word_0 = false;
if (++N_wall_connectors > 1)
{
print_word_0 = true;
break;
}
}
}
N_wall_connectors = 0;
for (j=0; j<N_links; j++)
{
if (ppla[j].rw == linkage->num_words-1)
{
if (easy_match(connector_string(ppla[j].lc), RIGHT_WALL_SUPPRESS))
print_word_N = false;
if (++N_wall_connectors > 1)
{
print_word_N = true;
break;
}
}
}
}
N_words_to_print = linkage->num_words;
if (!print_word_N) N_words_to_print--;
size_t *start = NULL;
char **picture = NULL;
char **xpicture = NULL;
size_t max_height = 0;
size_t max_bytes = set_centers(linkage, center, word_offset,
print_word_0, N_words_to_print) +1;
// num_cols is the total number of columns needed to display
// the ascii-art diagram, not counting the glyphs. It might
// be less, or it might be more than the total number of
// bytes in the UTF-8 string! Which one depends on just how
// much garage there might be in the string. Note also that,
// in general, the glyph of the last word will stick out past
// the num_cols here.
unsigned int num_cols = center[N_words_to_print-1]+1;
if (max_bytes < num_cols) max_bytes = num_cols;
diagram_alloc_tmpmem(&start, &picture, &xpicture,
&max_height, HEIGHT_INC,
max_bytes, num_cols);
top_row = 0;
// Longer links are printed above the lower links.
for (j=0; j<N_links; j++)
{
Link *lnk = ll[j].lnk;
if (!print_word_0 && (lnk->lw == 0)) continue;
/* Gets rid of the irrelevant link to the left wall */
if (!print_word_N && (lnk->rw == linkage->num_words-1)) continue;
/* Put it into the lowest position */
/* Keep in mind that cl, cr are "columns" not "bytes" */
cl = center[lnk->lw];
cr = center[lnk->rw];
for (row=0; row < max_height; row++)
{
for (k=cl+1; k<cr; k++)
{
if (picture[row][k] != ' ') break;
}
if (k == cr) break;
}
if (NULL != pctx) /* PS junk */
{
/* We know it fits, so put it in this row */
pctx->link_heights[j] = row;
}
if (2*row+2 > max_height-1) {
lgdebug(+9, "Extending rows up to %u.\n", (2*row+2)+HEIGHT_INC);
diagram_alloc_tmpmem(&start, &picture, &xpicture,
&max_height, (2*row+2)+HEIGHT_INC,
max_bytes, num_cols);
}
if (row > top_row) top_row = row;
for (k=cl+1; k<cr; k++) {
picture[row][k] = '-';
}
s = lnk->link_name;
k = strlen(s);
inc = cl + cr + 2;
if (inc < k) inc = 0;
else inc = (inc-k)/2;
if (inc <= cl) {
t = picture[row] + cl + 1;
} else {
t = picture[row] + inc;
}
/* Add direction indicator */
if (DEPT_CHR == connector_string(lnk->lc)[0] &&
(t > &picture[row][cl])) { picture[row][cl+1] = '<'; }
if (HEAD_CHR == connector_string(lnk->lc)[0]) { *(t-1) = '>'; }
/* Copy connector name; stop short if no room */
while ((*s != '\0') && (*t == '-')) *t++ = *s++;
/* Add direction indicator */
if (DEPT_CHR == connector_string(lnk->rc)[0]) { picture[row][cr-1] = '>'; }
if (HEAD_CHR == connector_string(lnk->rc)[0]) { *t = '<'; }
picture[row][cl] = '+';
picture[row][cr] = '+';
/* Now put in the | below this one, where needed */
for (k=0; k<row; k++) {
if (picture[k][cl] == ' ') {
picture[k][cl] = '|';
}
if (picture[k][cr] == ' ') {
picture[k][cr] = '|';
}
}
}
/* We have the link picture, now put in the words and extra "|"s */
t = xpicture[0];
if (print_word_0) k = 0; else k = 1;
for (; k<N_words_to_print; k++)
{
for (i = 0; i < (size_t) word_offset[k]; i++) *t++ = ' ';
// Copy raw bytes. Adjustments for different widths,
// invalid utf8 characters, etc. is done later.
s = linkage->word[k];
while (*s != '\0') { *t++ = *s++; }
*t++ = ' ';
}
*t = '\0';
/* If display_short is NOT true, then the linkage diagram is printed
* in the "tall" style, with an extra row of vertical descenders
* between each level. */
if (display_short) {
for (k=0; picture[0][k] != '\0'; k++) {
if ((picture[0][k] == '+') || (picture[0][k] == '|')) {
xpicture[1][k] = '|';
} else {
xpicture[1][k] = ' ';
}
}
xpicture[1][k] = '\0';
for (row=0; row < top_row+1; row++) {
strcpy(xpicture[row+2], picture[row]);
}
top_row += 2;
} else {
for (row=0; row < top_row+1; row++) {
strcpy(xpicture[2*row+2],picture[row]);
for (k=0; picture[row][k] != '\0'; k++) {
if ((picture[row][k] == '+') || (picture[row][k] == '|')) {
xpicture[2*row+1][k] = '|';
} else {
xpicture[2*row+1][k] = ' ';
}
}
xpicture[2*row+1][k] = '\0';
}
top_row = 2*top_row + 2;
}
/* We've built the picture, now print it out. */
/* Start locations, for each row. These may vary, due to different
* column-widths of utf8 glyphs. */
top_row_p1 = top_row + 1;
for (row = 0; row < top_row_p1; row++)
start[row] = 0;
if (NULL != pctx) /* PS junk */
{
pctx->N_rows = 0;
pctx->row_starts[pctx->N_rows] = 0;
pctx->N_rows++;
}
if (print_word_0) i = 0; else i = 1;
unsigned int c = 0; /* Character offset in the last word on a row. */
#define RIGHT_MARGIN 1
while (i < N_words_to_print)
{
unsigned int revrs;
/* Count the column-widths of the words, up to the max screen width.
* Use word_offset only for the initial part of the word. */
unsigned int uwidth = 0;
unsigned int wwid;
do {
wwid = ((c == 0) * word_offset[i])
+ utf8_strwidth(linkage->word[i]+c) + 1;
if (x_screen_width - RIGHT_MARGIN < uwidth + wwid) break;
uwidth += wwid;
c = 0;
i++;
} while (i < N_words_to_print);
if (i < N_words_to_print) {
/* The whole word doesn't fit - fit as much as possible from it. */
if (0 == uwidth)
{
uwidth = x_screen_width - RIGHT_MARGIN - (c == 0)*word_offset[i]-1;
c += utf8_chars_in_width(linkage->word[i]+c, uwidth);
}
if (NULL != pctx) /* PS junk */
{
pctx->row_starts[pctx->N_rows] = i - (!print_word_0);
pctx->N_rows++;
}
}
dyn_strcat(string, "\n");
/* print each row of the picture */
top_row_p1 = top_row + 1;
for (revrs = 0; revrs < top_row_p1; revrs++)
{
/*
* The `glyph_width` is the width, in columns, of the printable
* glyph. Chinese glyphs are almost always width two.
*/
size_t glyph_width = 0;
row = top_row - revrs;
// k is the number of bytes into the row that we previously
// printed. It should normally point at a valid UTF-8 char.
k = start[row];
for (j = k; (glyph_width < uwidth) && (xpicture[row][j] != '\0'); )
{
// If we don't have a glyph for this code-point,
// then assume the terminal will use a two-column-
// -wide "box font" with the hex code inside.
int gw = utf8_charwidth(&xpicture[row][j]);
if (gw < 0) gw = 2;
glyph_width += gw;
/* Copy exactly one multi-byte character to buf */
j += append_utf8_char(string, &xpicture[row][j]);
}
// Record exactly how much we printed, so we can
// resume here, if the diagram wraps.
start[row] = j;
// Hunt down and kill trailing blanks.
dyn_trimback(string);
dyn_strcat(string, "\n");
}
dyn_strcat(string, "\n");
}
diagram_free_tmpmem(start, picture, xpicture, max_height);
return dyn_str_take(string);
}
/**
* Print the indicated linkage as utf8-art into the given string.
* The width of the diagram is given by the terminal width, taken
* from the parse options.
*
* The returned string is malloced, and needs to be freed with
* linkage_free_diagram()
*/
char * linkage_print_diagram(const Linkage linkage, bool display_walls, size_t screen_width)
{
if (!linkage) return NULL;
return linkage_print_diagram_ctxt(linkage, display_walls, screen_width, NULL);
}
void linkage_free_diagram(char * s)
{
if (NULL == s) return;
exfree(s, strlen(s)+1);
}
void linkage_free_disjuncts(char * s)
{
exfree(s, strlen(s)+1);
}
void linkage_free_links_and_domains(char * s)
{
exfree(s, strlen(s)+1);
}
/* Forward declarations, the gunk is at the bottom. */
static const char * trailer(bool print_ps_header);
static const char * header(bool print_ps_header);
char * linkage_print_postscript(const Linkage linkage, bool display_walls, bool print_ps_header)
{
char * ps, * qs, * ascii;
int size;
/* call the ascii printer to initialize the row size stuff. */
ps_ctxt_t ctx;
ctx.link_heights = (int *) alloca(linkage->num_links * sizeof(int));
ctx.row_starts = (int *) alloca((linkage->num_words + 1) * sizeof(int));
ascii = linkage_print_diagram_ctxt(linkage, display_walls, 8000, &ctx);
linkage_free_diagram(ascii);
ps = build_linkage_postscript_string(linkage, display_walls, &ctx);
size = strlen(header(print_ps_header)) + strlen(ps) + strlen(trailer(print_ps_header)) + 1;
qs = (char *) exalloc(sizeof(char)*size);
snprintf(qs, size, "%s%s%s", header(print_ps_header), ps, trailer(print_ps_header));
exfree(ps, strlen(ps)+1);
return qs;
}
void linkage_free_postscript(char * s)
{
exfree(s, strlen(s)+1);
}
char * linkage_print_pp_msgs(Linkage linkage)
{
if (linkage && linkage->lifo.pp_violation_msg)
return strdup(linkage->lifo.pp_violation_msg);
return strdup("");
}
void linkage_free_pp_msgs(char * s)
{
exfree(s, strlen(s)+1);
}
void print_disjunct_counts(Sentence sent)
{
size_t i;
int dcnt;
int t = 0;
int lcnt = 0, rcnt = 0;
for (i=0; i<sent->length; i++)
{
Disjunct *d = sent->word[i].d;
dcnt = count_disjuncts(d);
rcnt += right_connector_count(d);
lcnt += left_connector_count(d);
t += dcnt;
/* XXX alternatives[0] is not really correct, here .. */
prt_error("%s(%d) ", sent->word[i].alternatives[0], dcnt);
}
prt_error("\n\\");
prt_error("Total: %d disjuncts, %d (%d+/%d-) connectors\n\n",
t, rcnt+lcnt, rcnt, lcnt);
}
static const char * trailer(bool print_ps_header)
{
static const char * trailer_string=
"diagram\n"
"\n"
"%%EndDocument\n"
;
if (print_ps_header) return trailer_string;
else return "";
}
static const char * header(bool print_ps_header)
{
static const char * header_string=
"%!PS-Adobe-2.0 EPSF-1.2\n"
"%%Pages: 1\n"
"%%BoundingBox: 0 -20 500 200\n"
"%%EndComments\n"
"%%BeginDocument: \n"
"\n"
"% compute size of diagram by adding\n"
"% #rows x 8.5\n"
"% (#rows -1) x 10\n"
"% \\sum maxheight x 10\n"
"/nulllink () def % The symbol of a null link\n"
"/wordfontsize 11 def % the size of the word font\n"
"/labelfontsize 9 def % the size of the connector label font\n"
"/ex 10 def % the horizontal radius of all the links\n"
"/ey 10 def % the height of the level 0 links\n"
"/ed 10 def % amount to add to this height per level\n"
"/radius 10 def % radius for rounded arcs\n"
"/row-spacing 10 def % the space between successive rows of the diagram\n"
"\n"
"/gap wordfontsize .5 mul def % the gap between words\n"
"/top-of-words wordfontsize .85 mul def\n"
" % the delta y above where the text is written where\n"
" % the major axis of the ellipse is located\n"
"/label-gap labelfontsize .1 mul def\n"
"\n"
"/xwordfontsize 10 def % the size of the word font\n"
"/xlabelfontsize 10 def % the size of the connector label font\n"
"/xex 10 def % the horizontal radius of all the links\n"
"/xey 10 def % the height of the level 0 links\n"
"/xed 10 def % amount to add to this height per level\n"
"/xradius 10 def % radius for rounded arcs\n"
"/xrow-spacing 10 def % the space between successive rows of the diagram\n"
"/xgap wordfontsize .5 mul def % the gap between words\n"
"\n"
"/centerpage 6.5 72 mul 2 div def\n"
" % this number of points from the left margin is the center of page\n"
"\n"
"/rightpage 6.5 72 mul def\n"
" % number of points from the left margin is the right margin\n"
"\n"
"/show-string-centered-dict 5 dict def\n"
"\n"
"/show-string-centered {\n"
" show-string-centered-dict begin\n"
" /string exch def\n"
" /ycenter exch def\n"
" /xcenter exch def\n"
" xcenter string stringwidth pop 2 div sub\n"
" ycenter labelfontsize .3 mul sub\n"
" moveto\n"
" string show\n"
" end\n"
"} def\n"
"\n"
"/clear-word-box {\n"
" show-string-centered-dict begin\n"
" /string exch def\n"
" /ycenter exch def\n"
" /xcenter exch def\n"
" newpath\n"
" /urx string stringwidth pop 2 div def\n"
" /ury labelfontsize .3 mul def\n"
" xcenter urx sub ycenter ury sub moveto\n"
" xcenter urx add ycenter ury sub lineto\n"
" xcenter urx add ycenter ury add lineto\n"
" xcenter urx sub ycenter ury add lineto\n"
" closepath\n"
" 1 setgray fill\n"
" 0 setgray\n"
" end\n"
"} def\n"
"\n"
"/diagram-sentence-dict 20 dict def\n"
"\n"
"/diagram-sentence-circle\n"
"{diagram-sentence-dict begin \n"
" /links exch def\n"
" /words exch def\n"
" /n words length def\n"
" /Times-Roman findfont wordfontsize scalefont setfont\n"
" /x 0 def\n"
" /y 0 def\n"
"\n"
" /left-ends [x dup words {stringwidth pop add gap add dup}\n"
" forall pop pop] def\n"
" /right-ends [x words {stringwidth pop add dup gap add} forall pop] def\n"
" /centers [0 1 n 1 sub {/i exch def\n"
" left-ends i get\n"
" right-ends i get\n"
" add 2 div\n"
" } for ] def\n"
"\n"
" x y moveto\n"
" words {show gap 0 rmoveto} forall\n"
"\n"
" .5 setlinewidth \n"
"\n"
" links {dup 0 get /leftword exch def\n"
" dup 1 get /rightword exch def\n"
" dup 2 get /level exch def\n"
" 3 get /string exch def\n"
" newpath\n"
" string nulllink eq {[2] 1 setdash}{[] 0 setdash} ifelse\n"
"% string nulllink eq {.8 setgray}{0 setgray} ifelse\n"
" centers leftword get\n"
" y top-of-words add\n"
" moveto\n"
" \n"
" centers rightword get\n"
" centers leftword get\n"
" sub 2 div dup\n"
" radius \n"
" lt {/radiusx exch def}{pop /radiusx radius def} ifelse\n"
" \n"
" \n"
" \n"
" centers leftword get\n"
" y top-of-words add ey ed level mul add add\n"
" centers rightword get\n"
" y top-of-words add ey ed level mul add add\n"
" radiusx\n"
" arcto\n"
" 4 {pop} repeat\n"
" centers rightword get\n"
" y top-of-words add ey ed level mul add add\n"
" centers rightword get\n"
" y top-of-words add\n"
" radiusx\n"
" arcto\n"
" 4 {pop} repeat\n"
" centers rightword get\n"
" y top-of-words add\n"
" lineto\n"
"\n"
" stroke\n"
"\n"
" /radius-y ey ed level mul add def\n"
"\n"
" /center-arc-x\n"
" centers leftword get centers rightword get add 2 div\n"
" def\n"
" \n"
" /center-arc-y\n"
" y top-of-words radius-y add add\n"
" def\n"
"\n"
" /Courier-Bold findfont labelfontsize scalefont setfont \n"
" center-arc-x center-arc-y string clear-word-box\n"
" center-arc-x center-arc-y string show-string-centered\n"
" } forall\n"
" end\n"
" } def\n"
"\n"
"/diagramdict 20 dict def\n"
"\n"
"/diagram\n"
"{diagramdict begin\n"
" /break-words exch def\n"
" /links exch def\n"
" /words exch def\n"
" /n words length def\n"
" /n-rows break-words length def\n"
" /Times-Roman findfont wordfontsize scalefont setfont\n"
"\n"
" /left-ends [0 dup words {stringwidth pop add gap add dup}\n"
" forall pop pop] def\n"
" /right-ends [0 words {stringwidth pop add dup gap add} forall pop] def\n"
"\n"
" /lwindows [ break-words {left-ends exch get gap 2 div sub } forall ] def\n"
" /rwindows [1 1 n-rows 1 sub {/i exch def\n"
" lwindows i get } for\n"
" right-ends n 1 sub get gap 2 div add\n"
" ] def\n"
"\n"
"\n"
" /max 0 def\n"
" 0 1 links length 1 sub {\n"
" /i exch def\n"
" /t links i get 2 get def\n"
" t max gt {/max t def} if\n"
" } for\n"
"\n"
" /max-height ed max mul ey add top-of-words add row-spacing add def\n"
" /total-height n-rows max-height mul row-spacing sub def\n"
"\n"
" /max-width 0 def % compute the widest window\n"
" 0 1 n-rows 1 sub {\n"
" /i exch def\n"
" /t rwindows i get lwindows i get sub def\n"
" t max-width gt {/max-width t def} if\n"
" } for\n"
"\n"
" centerpage max-width 2 div sub 0 translate % centers it\n"
" % rightpage max-width sub 0 translate % right justified\n"
" % Delete both of these to make it left justified\n"
"\n"
" n-rows 1 sub -1 0\n"
" {/i exch def\n"
" gsave\n"
" newpath\n"
" %/centering centerpage rwindows i get lwindows i get sub 2 div sub def\n"
" % this line causes each row to be centered\n"
" /centering 0 def\n"
" % set centering to 0 to prevent centering of each row \n"
"\n"
" centering -100 moveto % -100 because some letters go below zero\n"
" centering max-height n-rows mul lineto\n"
" rwindows i get lwindows i get sub centering add\n"
" max-height n-rows mul lineto\n"
" rwindows i get lwindows i get sub centering add\n"
" -100 lineto\n"
" closepath\n"
" clip\n"
" lwindows i get neg n-rows i sub 1 sub max-height mul translate\n"
" centerpage centering 0 translate\n"
" words links diagram-sentence-circle\n"
" grestore\n"
" } for\n"
" end\n"
"} def \n"
"\n"
"/diagramx\n"
"{diagramdict begin\n"
" /break-words exch def\n"
" /links exch def\n"
" /words exch def\n"
" /n words length def\n"
" /n-rows break-words length def\n"
" /Times-Roman findfont xwordfontsize scalefont setfont\n"
"\n"
" /left-ends [0 dup words {stringwidth pop add gap add dup}\n"
" forall pop pop] def\n"
" /right-ends [0 words {stringwidth pop add dup gap add} forall pop] def\n"
"\n"
" /lwindows [ break-words {left-ends exch get gap 2 div sub } forall ] def\n"
" /rwindows [1 1 n-rows 1 sub {/i exch def\n"
" lwindows i get } for\n"
" right-ends n 1 sub get xgap 2 div add\n"
" ] def\n"
"\n"
"\n"
" /max 0 def\n"
" 0 1 links length 1 sub {\n"
" /i exch def\n"
" /t links i get 2 get def\n"
" t max gt {/max t def} if\n"
" } for\n"
"\n"
" /max-height xed max mul xey add top-of-words add xrow-spacing add def\n"
" /total-height n-rows max-height mul xrow-spacing sub def\n"
"\n"
" /max-width 0 def % compute the widest window\n"
" 0 1 n-rows 1 sub {\n"
" /i exch def\n"
" /t rwindows i get lwindows i get sub def\n"
" t max-width gt {/max-width t def} if\n"
" } for\n"
"\n"
" centerpage max-width 2 div sub 0 translate % centers it\n"
" % rightpage max-width sub 0 translate % right justified\n"
" % Delete both of these to make it left justified\n"
"\n"
" n-rows 1 sub -1 0\n"
" {/i exch def\n"
" gsave\n"
" newpath\n"
" %/centering centerpage rwindows i get lwindows i get sub 2 div sub def\n"
" % this line causes each row to be centered\n"
" /centering 0 def\n"
" % set centering to 0 to prevent centering of each row \n"
"\n"
" centering -100 moveto % -100 because some letters go below zero\n"
" centering max-height n-rows mul lineto\n"
" rwindows i get lwindows i get sub centering add\n"
" max-height n-rows mul lineto\n"
" rwindows i get lwindows i get sub centering add\n"
" -100 lineto\n"
" closepath\n"
" clip\n"
" lwindows i get neg n-rows i sub 1 sub max-height mul translate\n"
" centerpage centering 0 translate\n"
" words links diagram-sentence-circle\n"
" grestore\n"
" } for\n"
" end\n"
"} def \n"
"\n"
"/ldiagram\n"
"{diagramdict begin\n"
" /break-words exch def\n"
" /links exch def\n"
" /words exch def\n"
" /n words length def\n"
" /n-rows break-words length def\n"
" /Times-Roman findfont wordfontsize scalefont setfont\n"
"\n"
" /left-ends [0 dup words {stringwidth pop add gap add dup}\n"
" forall pop pop] def\n"
" /right-ends [0 words {stringwidth pop add dup gap add} forall pop] def\n"
"\n"
" /lwindows [ break-words {left-ends exch get gap 2 div sub } forall ] def\n"
" /rwindows [1 1 n-rows 1 sub {/i exch def\n"
" lwindows i get } for\n"
" right-ends n 1 sub get gap 2 div add\n"
" ] def\n"
"\n"
"\n"
" /max 0 def\n"
" 0 1 links length 1 sub {\n"
" /i exch def\n"
" /t links i get 2 get def\n"
" t max gt {/max t def} if\n"
" } for\n"
"\n"
" /max-height ed max mul ey add top-of-words add row-spacing add def\n"
" /total-height n-rows max-height mul row-spacing sub def\n"
"\n"
" /max-width 0 def % compute the widest window\n"
" 0 1 n-rows 1 sub {\n"
" /i exch def\n"
" /t rwindows i get lwindows i get sub def\n"
" t max-width gt {/max-width t def} if\n"
" } for\n"
"\n"
" % centerpage max-width 2 div sub 0 translate % centers it\n"
" % rightpage max-width sub 0 translate % right justified\n"
" % Delete both of these to make it left justified\n"
"\n"
" n-rows 1 sub -1 0\n"
" {/i exch def\n"
" gsave\n"
" newpath\n"
" %/centering centerpage rwindows i get lwindows i get sub 2 div sub def\n"
" % this line causes each row to be centered\n"
" /centering 0 def\n"
" % set centering to 0 to prevent centering of each row \n"
"\n"
" centering -100 moveto % -100 because some letters go below zero\n"
" centering max-height n-rows mul lineto\n"
" rwindows i get lwindows i get sub centering add\n"
" max-height n-rows mul lineto\n"
" rwindows i get lwindows i get sub centering add\n"
" -100 lineto\n"
" closepath\n"
" clip\n"
" lwindows i get neg n-rows i sub 1 sub max-height mul translate\n"
" centerpage centering 0 translate\n"
" words links diagram-sentence-circle\n"
" grestore\n"
" } for\n"
" end\n"
"} def \n"
;
if (print_ps_header) return header_string;
else return "";
}
/**
* Print elements of the 2D-word-array produced for the parsers.
*
* - print_sentence_word_alternatives(s, sent, false, NULL, NULL, tokenpos)
* If a pointer to struct "tokenpos" is given, return through it the index of
* the first occurrence in the sentence of the given token. This is used to
* prevent duplicate information display for repeated morphemes (if there are
* multiples splits, each of several morphemes, otherwise some of them may
* repeat).
*
* - print_sentence_word_alternatives(s, sent, true, NULL, NULL, NULL)
* If debugprint is "true", this is a debug printout of the sentence. (The
* debug printouts are with level 0 because this function is invoked for debug
* on certain positive level.)
*
*
* - print_sentence_word_alternatives(s, sent, false, display_func, arg, NULL)
* Iterate over the sentence words and their alternatives. Handle each
* alternative using display_func(..., arg) if it is supplied, or else (if
* display_func is NULL) just print them. It is used to display disjunct
* information when command !!word is used.
* FIXME In the current version (using Wordgraph) the "alternatives" in the
* word-array don't necessarily consist of real word alternatives.
*
*/
struct tokenpos /* First position of the given token - to prevent duplicates */
{
const char * token;
size_t wi;
size_t ai;
};
void print_sentence_word_alternatives(dyn_str *s, Sentence sent,
bool debugprint,
char * (*display)(Dictionary, const char *, const void **),
const void **arg, struct tokenpos *tokenpos)
{
size_t wi; /* Internal sentence word index */
size_t ai; /* Index of a word alternative */
size_t sentlen = sent->length; /* Shortened if there is a right-wall */
size_t first_sentence_word = 0; /* Used for skipping a left-wall */
bool word_split = false; /* !!word got split */
Dictionary dict = sent->dict;
if (0 == sentlen)
{
/* It should not happen, but if it actually happens due to some
* strange conditions, it's better not to abort the program. */
prt_error("Error: Sentence length is 0 (reason unknown)\n");
return;
}
if (debugprint || (NULL != tokenpos))
; /* Do nothing */
else
{
/* For analyzing words we need to ignore the left/right walls */
if (dict->left_wall_defined &&
((NULL != sent->word[0].alternatives[0])) &&
(0 == strcmp(sent->word[0].alternatives[0], LEFT_WALL_WORD)))
first_sentence_word = 1;
if (dict->right_wall_defined &&
((NULL != sent->word[sentlen-1].alternatives[0])) &&
(0 == strcmp(sent->word[sentlen-1].alternatives[0], RIGHT_WALL_WORD)))
sentlen--;
/* Find if a word got split. This is indicated by:
* 1. More than one word in the sentence
* (no check if it actually results from !!w1 w2 ...).
* 2. A word containing more than one alternative. */
if (sentlen - first_sentence_word > 1)
{
word_split = true;
}
else
{
for (wi=first_sentence_word; wi<sentlen; wi++)
{
Word w = sent->word[wi];
/* There should always be at least one alternative */
assert((NULL != w.alternatives) && (NULL != w.alternatives[0]) &&
('\0' != w.alternatives[0][0]), "Missing alt for word %zu", wi);
if (NULL != w.alternatives[1])
{
word_split = true;
break;
}
}
}
/* "String", because it can be a word, morpheme, or idiom */
if (word_split && (NULL == display)) dyn_strcat(s, "String splits to:\n");
/* We used to print the alternatives of the word here, one per line.
* In the current (Wordgraph) version, the alternatives may look
* like nonsense combination of tokens - not as the strict split
* possibilities of words as in previous versions.
* E.g.: For Hebrew word "הכלב", we now get these "alternatives":
* ה= כלב לב ב=
* ה= כ= ל=
* ה= כ=
* For "'50s,"
* ' s s ,
* '50 50 ,
* '50s
* Clearly, this is not informative any more. Instead, one line with a
* list of tokens (without repetitions) is printed
* ה= כלב לב ב= כ= ל=
*
* FIXME Print the alternatives from the wordgraph.
*/
}
/* Iterate over sentence input words */
for (wi=first_sentence_word; wi<sentlen; wi++)
{
Word w = sent->word[wi];
size_t w_start = wi; /* input word index */
size_t max_nalt = 0;
#if 0 /* In the Wordgraph version firstupper and post_quote don't exist. */
if (debugprint) lgdebug(0, " word%d %c%c: %s\n ",
wi, w.firstupper ? 'C' : ' ', sent->post_quote[wi] ? 'Q' : ' ',
#endif
if (debugprint) append_string(s, " word%zu: %s\n", wi, w.unsplit_word);
/* There should always be at least one alternative */
assert((NULL != w.alternatives) && (NULL != w.alternatives[0]) &&
('\0' != w.alternatives[0][0]), "Missing alt for word %zu", wi);
//err_msg(lg_Debug, "word%zu '%s' nalts %zu\n",
// wi, sent->word[wi].unsplit_word, altlen(sent->word[wi].alternatives));
for (wi = w_start; (wi == w_start) ||
((wi < sentlen) && (! sent->word[wi].unsplit_word)); wi++)
{
size_t nalt = altlen(sent->word[wi].alternatives);
max_nalt = MAX(max_nalt, nalt);
}
/* Iterate over alternatives */
for (ai=0; ai < max_nalt; ai++)
{
if (debugprint)
{
if (0 < ai) dyn_strcat(s, "\n");
append_string(s, " alt%zu:", ai);
}
int missing = 0;
for (wi = w_start; (wi == w_start) ||
((wi < sentlen) && (! sent->word[wi].unsplit_word)); wi++)
{
size_t nalts = altlen(sent->word[wi].alternatives);
const char *wt;
/* Some slots may be shorter then others. To avoid a
* misleading debug display, print "[missing]" for them,
* but only if there are tokens after them. */
if (ai >= nalts)
{
missing++;
continue;
}
wt = sent->word[wi].alternatives[ai];
/* Don't display information again for the same word */
if ((NULL != tokenpos) && (0 == strcmp(tokenpos->token, wt)))
{
tokenpos->wi = wi;
tokenpos->ai = ai;
return;
}
if (!debugprint)
{
struct tokenpos firstpos = { wt };
print_sentence_word_alternatives(s, sent, false, NULL, NULL, &firstpos);
if (((firstpos.wi != wi) || (firstpos.ai != ai)) &&
firstpos.wi >= first_sentence_word) // allow !!LEFT_WORD
{
/* We encountered this token earlier */
if (NULL != display)
lgdebug(6, "Skipping repeated %s\n", wt);
continue;
}
}
if (debugprint)
{
while (missing-- > 0) append_string(s, " %s", "[missing]");
const char *opt_start = "", *opt_end = "";
if (sent->word[wi].optional)
{
opt_start = "{";
opt_end = "}";
}
if ('\0' == wt[0]) wt = "'\\0'"; /* Reveal a bogus value. */
append_string(s, " %s%s%s", opt_start, wt, opt_end);
}
/* For now each word component is called "Token".
* TODO: Its type can be decoded and a more precise
* term (stem, prefix, etc.) can be used.
* Display the features of the token. */
if ((NULL == tokenpos) && (NULL != display))
{
char *info = display(sent->dict, wt, arg);
if (NULL == info) return;
append_string(s, "Token \"%s\" ", wt);
/* Cannot use append_string() for "info" because it may be
* a multi-MB string due to !!*
* (printing all dictionary words). */
dyn_strcat(s, info);
free(info);
}
else if (word_split) append_string(s, " %s", wt);
}
}
wi--;
/* Line separation after the tokens printed by "String splits to:". */
if (word_split && (NULL == display)) dyn_strcat(s, "\n\n");
if (debugprint) dyn_strcat(s, "\n");
}
if (debugprint) dyn_strcat(s, "\n");
}
// Use for debug and error printing.
void print_sentence_context(Sentence sent, dyn_str *outbuf)
{
size_t i, j;
const char **a, **b;
dyn_strcat(outbuf,
"\tFailing sentence contains the following words/morphemes:\n\t");
for (i=0; i<sent->length; i++)
{
for (a = sent->word[i].alternatives; NULL != *a; a++)
{
bool next_word = false;
for (j=0; j<sent->length; j++)
{
for (b = sent->word[j].alternatives; NULL != *b; b++)
{
/* print only the first occurrence. */
if (0 == strcmp(*a, *b))
{
next_word = true;
if (a != b) break;
dyn_strcat(outbuf, *a);
dyn_strcat(outbuf, " ");
break;
}
}
if (next_word) break;
}
}
}
dyn_strcat(outbuf, "\n");
}