Commit fb4154b3 authored by Matthias Simon's avatar Matthias Simon
Browse files

Match version-5 indent when exporting listings, examples, and tables.

Spaces drift off the PL tab stops, and an example title needs a tab to reach the EX column.

Co-authored-by: default avatarCursor <cursoragent@cursor.com>
parent 684b5427
Loading
Loading
Loading
Loading
+1 −1
Changes for tools/cmd/t3s/main_test.go: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -196,7 +196,7 @@ func TestExport(t *testing.T) {
	if strings.Contains(docXML, "hidden") {
		t.Fatal("test code was exported")
	}
	if !strings.Contains(docXML, "EXAMPLE 1: add") || !strings.Contains(docXML, `w:val="PL"`) {
	if !strings.Contains(docXML, "EXAMPLE 1:</w:t></w:r><w:r><w:tab/></w:r>") || !strings.Contains(docXML, ">add</w:t>") || !strings.Contains(docXML, `w:val="PL"`) {
		t.Fatal("example was not written in ETSI styles")
	}

+63 −6
Changes for tools/internal/doc/docx/code.go: 63 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -12,6 +12,12 @@ import (
// Code runs are Courier New 8pt, the PL style. Keywords are bold. TTCN-3
// comments are italic grey, and string and numeric literals are tinted.
// EBNF comments and nonterminals stay italic, without colour.
//
// PL tab stops are every 384 twips, which is four characters of Courier New
// 8pt. Listings indent with those tabs. Spaces in their place drift once a
// character grid is applied, which is how the published core-language PDF
// lays text out. A TTCN-3 line also starts one tab in, so the listing sits
// just to the right of the EXAMPLE label rather than on the left margin.
var (
	codePlain   = props{mono: true, size: 16}
	codeBold    = props{mono: true, bold: true, size: 16}
@@ -25,6 +31,13 @@ var (
// PL run, without the body-text gap of a Normal paragraph.
const codeGap = `<w:spacing w:before="0" w:after="0"/>`

// codeTabCols is the width of one PL tab stop in Courier New 8pt characters.
// codeTabTwips is that stop in twips (4 × 96).
const (
	codeTabCols  = 4
	codeTabTwips = 384
)

var prodHead = regexp.MustCompile(`(?m)^[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*::=`)

var ebnfDef = regexp.MustCompile(`^(\s*)([A-Za-z_][A-Za-z0-9_]*)(\s*::=\s*)(.*)$`)
@@ -40,24 +53,44 @@ func (w *writer) code(c *doc.Code, p place) error {
	w.prevCode = true
	if v, ok := metaValue(c, "example"); ok {
		w.exampleN++
		caption := fmt.Sprintf("EXAMPLE %d:", w.exampleN)
		label := fmt.Sprintf("EXAMPLE %d:", w.exampleN)
		// The EX hanging indent is the title column. A tab lands there, as it
		// does for NOTE. A space leaves the title short of that column.
		if v != "" {
			caption += " " + v
			if err := w.labeled("EX", `<w:keepNext/>`, label, []doc.Inline{doc.Text(v)}); err != nil {
				return err
			}
		} else {
			w.openPara("EX", `<w:keepNext/>`)
		w.text(caption, props{})
			w.text(label, props{})
			w.closePara()
		}
	}
	lines := c.Lines
	if len(lines) == 0 {
		lines = []string{""}
	}
	extra := `<w:keepLines/>`
	if p.level >= 0 {
		extra += fmt.Sprintf(`<w:ind w:left="%d"/>`, listIndent(p.level))
	inset := 0
	if ttcn3Lang(c.Lang) {
		inset = 1
	}
	var inComment bool
	for _, line := range lines {
		line = strings.TrimSuffix(line, "\r")
		level, rest := codeLevels(line)
		if inset > 0 && strings.TrimSpace(line) != "" {
			level += inset
		}
		extra := `<w:keepLines/>`
		if p.level >= 0 {
			// Tab stops are measured from the page margin. Inside a list the
			// paragraph indent is already past the first stops, so the inset
			// is added to that indent instead of being a tab.
			extra += fmt.Sprintf(`<w:ind w:left="%d"/>`, listIndent(p.level)+codeTabTwips*level)
			line = rest
		} else if level > 0 {
			line = strings.Repeat("\t", level) + rest
		}
		w.openPara("PL", extra)
		w.codeLine(c.Lang, line, &inComment)
		w.closePara()
@@ -65,6 +98,30 @@ func (w *writer) code(c *doc.Code, p place) error {
	return nil
}

// codeLevels splits a leading run of spaces and tabs into PL tab levels.
// A tab is four columns. Leftover columns stay spaces on rest.
func codeLevels(line string) (level int, rest string) {
	cols := 0
	i := 0
	for i < len(line) {
		switch line[i] {
		case ' ':
			cols++
			i++
		case '\t':
			cols += codeTabCols - cols%codeTabCols
			i++
		default:
			return cols / codeTabCols, strings.Repeat(" ", cols%codeTabCols) + line[i:]
		}
	}
	return cols / codeTabCols, strings.Repeat(" ", cols%codeTabCols)
}

func ttcn3Lang(lang string) bool {
	return strings.EqualFold(lang, "ttcn3")
}

func (w *writer) codeLine(lang, line string, inComment *bool) {
	switch strings.ToLower(lang) {
	case "ttcn3":
+5 −0
Changes for tools/internal/doc/docx/inline.go: 5 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -47,6 +47,11 @@ func expand(spans []span) []cell {
			continue
		}
		for _, r := range s.text {
			// A tab in prose is a separator. Code lines expand tabs before
			// they reach this point.
			if r == '\t' {
				r = ' '
			}
			out = append(out, cell{r: r, bold: s.bold, italic: s.italic, mono: s.mono, url: s.url})
		}
	}
+1 −1
Changes for tools/internal/doc/docx/ooxml.go: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -519,7 +519,7 @@ func (p *parser) run(dec *xml.Decoder) []span {
		case "t":
			b.WriteString(p.text(dec))
		case "tab":
			b.WriteByte(' ')
			b.WriteByte('\t')
			p.skip(dec)
		case "br", "cr":
			flush()
+35 −1
Changes for tools/internal/doc/docx/read.go: 35 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -95,6 +95,9 @@ func (r *reader) build(d *doc.Document) {
		} else if ebnfRE.MatchString(strings.Join(lines, "\n")) {
			lang = "ebnf"
		}
		if lang == "ttcn3" {
			lines = trimListingIndent(lines)
		}
		top().add(&doc.Code{
			Pos:     r.pos(start),
			EndLine: end,
@@ -392,13 +395,44 @@ func paraLines(spans []span) []string {
			if i > 0 {
				flush()
			}
			b.WriteString(part)
			b.WriteString(strings.ReplaceAll(part, "\t", "    "))
		}
	}
	flush()
	return lines
}

// trimListingIndent removes the one tab ttcn3 listings gain on export.
// Markdown stores the indent relative to the listing. The DOCX inset is
// presentation, so a round trip does not accumulate tabs.
func trimListingIndent(lines []string) []string {
	for _, ln := range lines {
		if strings.TrimSpace(ln) == "" {
			continue
		}
		if leadingSpaces(ln) < codeTabCols {
			return lines
		}
	}
	out := make([]string, len(lines))
	for i, ln := range lines {
		if strings.TrimSpace(ln) == "" {
			out[i] = ""
			continue
		}
		out[i] = ln[codeTabCols:]
	}
	return out
}

func leadingSpaces(s string) int {
	n := 0
	for n < len(s) && s[n] == ' ' {
		n++
	}
	return n
}

func trimBlank(lines []string) []string {
	for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" {
		lines = lines[1:]
Loading