Commit 684b5427 authored by Matthias Simon's avatar Matthias Simon
Browse files

Colour TTCN-3 comments and literals in exported listings.

Co-authored-by: default avatarCursor <cursoragent@cursor.com>
parent 3594d6f1
Loading
Loading
Loading
Loading
+64 −8
Changes for tools/internal/doc/docx/code.go: 64 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -9,13 +9,16 @@ import (
	"tools/internal/doc"
)

// Code runs are Courier New 8pt, the PL style. Keywords are bold and comments
// are italic, which is the highlighting used for syntactic descriptions in the
// working documents. Colours are not added.
// 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.
var (
	codePlain   = props{mono: true, size: 16}
	codeBold    = props{mono: true, bold: true, size: 16}
	codeItalic  = props{mono: true, italic: true, size: 16}
	codeComment = props{mono: true, italic: true, size: 16, color: "595959"}
	codeString  = props{mono: true, size: 16, color: "803000"}
	codeNumber  = props{mono: true, size: 16, color: "003B8F"}
)

// An empty Normal paragraph between two listings keeps them from becoming one
@@ -99,16 +102,16 @@ func (w *writer) ttcn3Segment(s string, inComment *bool) {
	for i := 0; i < len(s); {
		if *inComment {
			if j := strings.Index(s[i:], "*/"); j >= 0 {
				w.text(s[i:i+j+2], codeItalic)
				w.text(s[i:i+j+2], codeComment)
				i += j + 2
				*inComment = false
				continue
			}
			w.text(s[i:], codeItalic)
			w.text(s[i:], codeComment)
			return
		}
		if strings.HasPrefix(s[i:], "//") {
			w.text(s[i:], codeItalic)
			w.text(s[i:], codeComment)
			return
		}
		if strings.HasPrefix(s[i:], "/*") {
@@ -117,7 +120,16 @@ func (w *writer) ttcn3Segment(s string, inComment *bool) {
		}
		if s[i] == '"' || s[i] == '\'' {
			j := scanQuoted(s, i)
			w.text(s[i:j], codePlain)
			if s[i] == '\'' && j < len(s) && isStringSuffix(s[j]) {
				j++
			}
			w.text(s[i:j], codeString)
			i = j
			continue
		}
		if isDigit(s[i]) {
			j := scanNumber(s, i)
			w.text(s[i:j], codeNumber)
			i = j
			continue
		}
@@ -135,7 +147,7 @@ func (w *writer) ttcn3Segment(s string, inComment *bool) {
			continue
		}
		j := i + 1
		for j < len(s) && !codeSpecial(s, j) {
		for j < len(s) && !ttcn3Special(s, j) {
			j++
		}
		w.text(s[i:j], codePlain)
@@ -281,6 +293,50 @@ func codeSpecial(s string, i int) bool {
	return strings.HasPrefix(s[i:], "//") || strings.HasPrefix(s[i:], "/*")
}

func ttcn3Special(s string, i int) bool {
	return codeSpecial(s, i) || i < len(s) && isDigit(s[i])
}

func isDigit(c byte) bool { return c >= '0' && c <= '9' }

// isStringSuffix reports whether c is a bitstring, hexstring, or octetstring suffix.
func isStringSuffix(c byte) bool {
	switch c {
	case 'B', 'b', 'H', 'h', 'O', 'o':
		return true
	default:
		return false
	}
}

// scanNumber returns the index past a decimal integer or float starting at i.
func scanNumber(s string, i int) int {
	j := i
	for j < len(s) && isDigit(s[j]) {
		j++
	}
	if j+1 < len(s) && s[j] == '.' && isDigit(s[j+1]) {
		j++
		for j < len(s) && isDigit(s[j]) {
			j++
		}
	}
	if j < len(s) && (s[j] == 'e' || s[j] == 'E') {
		k := j + 1
		if k < len(s) && (s[k] == '+' || s[k] == '-') {
			k++
		}
		start := k
		for k < len(s) && isDigit(s[k]) {
			k++
		}
		if k > start {
			j = k
		}
	}
	return j
}

func scanQuoted(s string, i int) int {
	q := s[i]
	i++
+8 −4
Changes for tools/internal/doc/docx/write.go: 8 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -90,9 +90,9 @@ const (
// and a tab before the title. Notes use NO, example captions use EX, and code
// uses PL. Bullets use B1+ and ordered lists use BN. Code marked test is
// omitted, and so are HTML comments. An example caption is "EXAMPLE n: title",
// counted again from 1 in each clause. TTCN-3 keywords are bold and comments
// are italic. A defined EBNF name is italic, and a use of it is a hyperlink
// to the first definition.
// counted again from 1 in each clause. TTCN-3 keywords are bold, comments are
// italic grey, and string and numeric literals are coloured. A defined EBNF
// name is italic, and a use of it is a hyperlink to the first definition.
func Write(w io.Writer, d *doc.Document) error {
	if d == nil {
		return fmt.Errorf("docx: nil document")
@@ -163,6 +163,7 @@ type props struct {
	bold, italic, mono bool
	size               int // half-points; 0 inherits
	style              string
	color              string // RRGGBB; empty inherits
}

func (w *writer) document() []byte {
@@ -609,7 +610,7 @@ func (w *writer) tab(p props) {
func (w *writer) br() { w.body.WriteString(`<w:r><w:br/></w:r>`) }

func (p props) write(b *strings.Builder) {
	if p.style == "" && !p.mono && !p.bold && !p.italic && p.size == 0 {
	if p.style == "" && !p.mono && !p.bold && !p.italic && p.size == 0 && p.color == "" {
		return
	}
	b.WriteString(`<w:rPr>`)
@@ -625,6 +626,9 @@ func (p props) write(b *strings.Builder) {
	if p.italic {
		b.WriteString(`<w:i/><w:iCs/>`)
	}
	if p.color != "" {
		fmt.Fprintf(b, `<w:color w:val="%s"/>`, p.color)
	}
	if p.size > 0 {
		fmt.Fprintf(b, `<w:sz w:val="%d"/><w:szCs w:val="%d"/>`, p.size, p.size)
	}
+37 −1
Changes for tools/internal/doc/docx/write_test.go: 37 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -106,7 +106,7 @@ func TestWriteRoundTrip(t *testing.T) {
		`w:name="ebnf_Expr"`,
		`w:name="ebnf_Term"`,
		`<w:b/><w:bCs/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>var</w:t>`,
		`<w:i/><w:iCs/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>// note</w:t>`,
		`<w:i/><w:iCs/><w:color w:val="595959"/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>// note</w:t>`,
	} {
		if !strings.Contains(docXML, want) {
			t.Errorf("document missing %s", want)
@@ -220,6 +220,42 @@ func TestWriteRoundTrip(t *testing.T) {
	}
}

func TestTTCN3Highlight(t *testing.T) {
	d := &doc.Document{Sections: []*doc.Section{{
		Number: "1",
		Level:  1,
		Title:  []doc.Inline{doc.Text("Literals")},
		Blocks: []doc.Block{
			&doc.Code{Lang: "ttcn3", Lines: []string{
				`var integer x := 1; // note`,
				`x := "hi" & '01'B; /* block`,
				`still */ y := 1.5E-2;`,
			}},
			&doc.Code{Lang: "ebnf", Lines: []string{"Expr ::= Term. // keep"}},
		},
	}}}
	var buf bytes.Buffer
	if err := Write(&buf, d); err != nil {
		t.Fatal(err)
	}
	docXML := zipPart(t, buf.Bytes(), "word/document.xml")
	for _, want := range []string{
		`<w:b/><w:bCs/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>var</w:t>`,
		`<w:color w:val="003B8F"/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>1</w:t>`,
		`<w:i/><w:iCs/><w:color w:val="595959"/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>// note</w:t>`,
		`<w:color w:val="803000"/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>"hi"</w:t>`,
		`<w:color w:val="803000"/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>'01'B</w:t>`,
		`<w:i/><w:iCs/><w:color w:val="595959"/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>/* block</w:t>`,
		`<w:i/><w:iCs/><w:color w:val="595959"/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>still */</w:t>`,
		`<w:color w:val="003B8F"/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>1.5E-2</w:t>`,
		`<w:i/><w:iCs/><w:sz w:val="16"/><w:szCs w:val="16"/></w:rPr><w:t>// keep</w:t>`,
	} {
		if !strings.Contains(docXML, want) {
			t.Errorf("document missing %s", want)
		}
	}
}

func TestListContinuation(t *testing.T) {
	d := &doc.Document{Sections: []*doc.Section{{
		Number: "1",