gogcli/internal/cmd/docs_markdown_test.go
Gonçalo Alves 7945602f15
feat: add docs update command for editing Google Docs (#219)
* feat: add docs update command for editing Google Docs

* fix: handle document content range correctly for replace

* docs: add Jarbas avatar

* feat(gogcli): add markdown formatting support for Google Docs

Phase 1 & 2 complete:
- Markdown parser supporting headings, lists, code blocks, blockquotes, links
- Google Docs API integration for formatting
- --format markdown flag for docs update command
- Heading styles (H1-H6), horizontal rules, list indentation
- Code blocks with monospace font

Pending (Phase 3):
- Inline formatting (bold, italic, inline code) - index calculation issues
- Links - index calculation issues

Usage:
  gog docs update <docId> --content-file ./doc.md --format markdown

* fix(gogcli): fix inline formatting indices in markdown formatter

- Simplified document generation to avoid index calculation errors
- Fixed ParseInlineFormatting to correctly track positions
- Preserves: headings, code blocks, blockquotes, lists, horizontal rules

Pending: inline formatting (bold, italic, code, links) - indices still need work

* fix(gogcli): use UTF-16 code units for Google Docs API indexing

- Fixed markdown formatter to use UTF-16 code units instead of UTF-8 bytes
- Added utf16Len() helper function for accurate character counting
- Fixed inline formatting indices (bold, italic, code, links)
- Added empty line handling (MDEmptyLine)
- Successfully tested with Docker course doc (21KB, emojis, diagrams)

This resolves index mismatch errors caused by multi-byte characters like emojis
which are 4 bytes in UTF-8 but 2 code units in UTF-16.

* feat(gogcli): add slides commands with markdown support

- Add 'gog slides update' command with markdown formatting
- Create slides_formatter.go for Google Slides API batch updates
- Create slides_markdown.go for markdown parsing (titles, bullets, code)
- Add slides.go with update/create/read operations
- Update googleauth service for Slides scope

Related: PR #219

* fix(gogcli): use shapes for slides text boxes instead of direct insertion

- Fixed slides creation to use CreateShape with TEXT_BOX instead of inserting text directly
- Direct text insertion into slides is not supported by Google Slides API
- Added title text box with bold 36pt font
- Added body text box for content (bullets, paragraphs, code)
- Supports markdown formatting (bold, bullets, code blocks)

Tested: Successfully created 20-slide presentation from Docker course outline

* feat: add markdown table support (formatted text output)

* feat: implement native Google Docs table insertion with multi-step API

* feat(slides): add --template flag for creating presentations from templates

* fix: stabilize docs/slides markdown + auth flow (#219) (thanks @goncaloalves)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-02-13 22:52:17 +01:00

184 lines
4.4 KiB
Go

package cmd
import (
"testing"
)
func TestParseMarkdown(t *testing.T) {
tests := []struct {
name string
input string
expected []MarkdownElementType
}{
{
name: "heading 1",
input: "# Hello World",
expected: []MarkdownElementType{MDHeading1},
},
{
name: "heading 2",
input: "## Hello World",
expected: []MarkdownElementType{MDHeading2},
},
{
name: "paragraph",
input: "This is a paragraph",
expected: []MarkdownElementType{MDParagraph},
},
{
name: "bullet list",
input: "- Item 1\n- Item 2",
expected: []MarkdownElementType{MDListItem, MDListItem},
},
{
name: "numbered list",
input: "1. First\n2. Second",
expected: []MarkdownElementType{MDNumberedList, MDNumberedList},
},
{
name: "code block",
input: "```\ncode here\n```",
expected: []MarkdownElementType{MDCodeBlock},
},
{
name: "blockquote",
input: "> This is a quote",
expected: []MarkdownElementType{MDBlockquote},
},
{
name: "mixed content",
input: "# Title\n\nParagraph here\n\n- List item",
expected: []MarkdownElementType{MDHeading1, MDParagraph, MDListItem},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ParseMarkdown(tt.input)
if len(result) != len(tt.expected) {
t.Errorf("ParseMarkdown() got %d elements, want %d", len(result), len(tt.expected))
return
}
for i, el := range result {
if el.Type != tt.expected[i] {
t.Errorf("ParseMarkdown()[%d] = %v, want %v", i, el.Type, tt.expected[i])
}
}
})
}
}
func TestParseInlineFormatting(t *testing.T) {
tests := []struct {
name string
input string
expectedText string
expectedCount int
}{
{
name: "bold text",
input: "This is **bold** text",
expectedText: "This is bold text",
expectedCount: 1,
},
{
name: "italic text",
input: "This is *italic* text",
expectedText: "This is italic text",
expectedCount: 1,
},
{
name: "code text",
input: "This is `code` text",
expectedText: "This is code text",
expectedCount: 1,
},
{
name: "link",
input: "Check [this link](https://example.com)",
expectedText: "Check this link",
expectedCount: 1,
},
{
name: "no formatting",
input: "Just plain text",
expectedText: "Just plain text",
expectedCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
styles, text := ParseInlineFormatting(tt.input)
if text != tt.expectedText {
t.Errorf("ParseInlineFormatting() text = %q, want %q", text, tt.expectedText)
}
if len(styles) != tt.expectedCount {
t.Errorf("ParseInlineFormatting() got %d styles, want %d", len(styles), tt.expectedCount)
}
})
}
}
func TestParseHeading(t *testing.T) {
tests := []struct {
line string
expectedLevel int
expectedContent string
}{
{"# Title", 1, "Title"},
{"## Subtitle", 2, "Subtitle"},
{"### Section", 3, "Section"},
{"#### Subsection", 4, "Subsection"},
{"Not a heading", 0, ""},
{"#No space", 0, ""},
}
for _, tt := range tests {
level, content := parseHeading(tt.line)
if level != tt.expectedLevel {
t.Errorf("parseHeading(%q) level = %d, want %d", tt.line, level, tt.expectedLevel)
}
if content != tt.expectedContent {
t.Errorf("parseHeading(%q) content = %q, want %q", tt.line, content, tt.expectedContent)
}
}
}
func TestIsHorizontalRule(t *testing.T) {
tests := []struct {
line string
expected bool
}{
{"---", true},
{"***", true},
{"___", true},
{"- - -", true},
{"* * *", true},
{"--", false},
{"---text", false},
{"text---", false},
}
for _, tt := range tests {
result := isHorizontalRule(tt.line)
if result != tt.expected {
t.Errorf("isHorizontalRule(%q) = %v, want %v", tt.line, result, tt.expected)
}
}
}
func TestParseMarkdown_TableDoesNotSkipFollowingLine(t *testing.T) {
input := "| Name | Value |\n| --- | --- |\n| a | b |\nAfter table"
got := ParseMarkdown(input)
if len(got) != 2 {
t.Fatalf("expected 2 elements, got %d", len(got))
}
if got[0].Type != MDTable {
t.Fatalf("first element type = %v, want %v", got[0].Type, MDTable)
}
if got[1].Type != MDParagraph || got[1].Content != "After table" {
t.Fatalf("second element = %#v, want paragraph 'After table'", got[1])
}
}