diff --git a/go.mod b/go.mod index 9d7170ff5..26497138b 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.23.0 require ( github.com/Microsoft/go-winio v0.6.2 - github.com/beevik/etree v1.5.1 github.com/bmatcuk/doublestar/v4 v4.10.0 github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 diff --git a/go.sum b/go.sum index 4e80b061f..7e42f3619 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= -github.com/beevik/etree v1.5.1 h1:TC3zyxYp+81wAmbsi8SWUpZCurbxa6S8RITYRSkNRwo= -github.com/beevik/etree v1.5.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= diff --git a/shortcuts/slides/slides_xml_get.go b/shortcuts/slides/slides_xml_get.go index f56dd562b..8a18b0997 100644 --- a/shortcuts/slides/slides_xml_get.go +++ b/shortcuts/slides/slides_xml_get.go @@ -7,12 +7,8 @@ import ( "bytes" "context" "fmt" - "regexp" - "strconv" "strings" - "github.com/beevik/etree" - "github.com/larksuite/cli/errs" "github.com/larksuite/cli/extension/fileio" "github.com/larksuite/cli/internal/validate" @@ -20,10 +16,9 @@ import ( ) // SlidesXMLGet fetches the full XML presentation content. When --output is -// provided it writes reindented XML to a local file, and --raw prints -// reindented XML to stdout; otherwise it returns the server's original -// content unmodified in the standard JSON envelope. Use --slide-id or -// --slide-number to fetch one page. +// provided it writes to a local file; otherwise it returns the XML in the +// standard JSON envelope. Use --slide-id or --slide-number to fetch one page, +// and use --raw for direct XML stdout. var SlidesXMLGet = common.Shortcut{ Service: "slides", Command: "+xml-get", @@ -35,8 +30,8 @@ var SlidesXMLGet = common.Shortcut{ AuthTypes: []string{"user", "bot"}, Flags: []common.Flag{ {Name: "presentation", Desc: "xml_presentation_id, slides URL, or wiki URL that resolves to slides", Required: true}, - {Name: "output", Desc: "local XML output path; the saved file is formatted for readability; must be a relative path within the current directory; existing file is overwritten; omit to return the server's original XML in the JSON envelope"}, - {Name: "raw", Type: "bool", Desc: "print formatted XML to stdout without the JSON envelope; incompatible with --output and --jq"}, + {Name: "output", Desc: "local XML output path; must be a relative path within the current directory; existing file is overwritten; omit to return XML in the JSON envelope"}, + {Name: "raw", Type: "bool", Desc: "print raw XML to stdout instead of the JSON envelope; incompatible with --output and --jq"}, {Name: "slide-id", Desc: "slide page identifier; omit both slide selectors to fetch full presentation XML"}, {Name: "slide-number", Type: "int", Desc: "1-based slide page number; omit both slide selectors to fetch full presentation XML"}, {Name: "revision-id", Type: "int", Default: "-1", Desc: "presentation revision_id; -1 means latest"}, @@ -113,10 +108,10 @@ var SlidesXMLGet = common.Shortcut{ } dry.GET(path).Params(params) if outputPath := strings.TrimSpace(runtime.Str("output")); outputPath != "" { - return dry.Set("output", outputPath).Set("stdout_content", "suppressed; formatted XML content is saved to --output during execution") + return dry.Set("output", outputPath).Set("stdout_content", "suppressed; XML content is saved to --output during execution") } if runtime.Bool("raw") { - return dry.Set("output", "").Set("stdout_content", "formatted XML content is printed to stdout during execution") + return dry.Set("output", "").Set("stdout_content", "raw XML content is printed to stdout during execution") } return dry.Set("output", "").Set("stdout_content", "JSON envelope with XML content is printed to stdout during execution") }, @@ -255,31 +250,22 @@ func fetchSlidesXMLGetContent(runtime *common.RuntimeContext, presentationID str return content, out, nil } -// outputSlidesXMLGetContent routes the fetched XML to its output surface. -// Only the text surfaces are reindented: --raw stdout and --output files are -// read directly by humans and line tools. The JSON envelope carries the -// server content verbatim instead -- inside a JSON string every newline is -// escaped to \n, so formatting there buys no readability and only inflates -// the payload, while passthrough keeps that read path byte-exact without -// even parsing the content. func outputSlidesXMLGetContent(runtime *common.RuntimeContext, content string, outputPath string, out map[string]interface{}) error { if outputPath == "" { if !runtime.Bool("raw") { runtime.OutFormatRaw(out, nil, nil) return nil } - formatted, _ := prettyPrintXMLOrOriginal(runtime, content) - if _, err := fmt.Fprint(runtime.IO().Out, formatted); err != nil { + if _, err := fmt.Fprint(runtime.IO().Out, content); err != nil { return errs.NewInternalError(errs.SubtypeFileIO, "write XML content to stdout: %v", err).WithCause(err) } return nil } - formatted, prettyPrinted := prettyPrintXMLOrOriginal(runtime, content) result, err := runtime.FileIO().Save(outputPath, fileio.SaveOptions{ ContentType: "application/xml", - ContentLength: int64(len(formatted)), - }, bytes.NewReader([]byte(formatted))) + ContentLength: int64(len(content)), + }, bytes.NewReader([]byte(content))) if err != nil { return common.WrapSaveErrorTyped(err) } @@ -294,7 +280,6 @@ func outputSlidesXMLGetContent(runtime *common.RuntimeContext, content string, o "path": resolvedPath, "size": result.Size(), "content_saved": true, - "pretty_printed": prettyPrinted, } for _, key := range []string{"revision_id", "remove_attr_id", "slide_id", "slide_number"} { if value, ok := out[key]; ok { @@ -304,192 +289,3 @@ func outputSlidesXMLGetContent(runtime *common.RuntimeContext, content string, o runtime.Out(fileOut, nil) return nil } - -// textBearingTags are the SML elements whose schema content model is -// mixed (arbitrary text interleaved with inline markup): the

paragraph -// container and its inline formatting children, plus chart title/subtitle. -// See slides_xml_schema_definition.xml,

element docs: a deliberate space -// or tab is represented via / character references. Those references -// are masked before XML parsing so etree cannot resolve away their lexical -// representation, and reindentStructural never descends into these elements. -var textBearingTags = map[string]bool{ - "p": true, - "strong": true, - "em": true, - "u": true, - "span": true, - "del": true, - "a": true, - "shadow": true, - "outline": true, - "chartTitle": true, - "chartSubTitle": true, -} - -// prettyPrintXML reindents xmlContent so structural elements (presentation, -// slide, shape, style, ...) each sit on their own line. The server returns -// XML as a single unbroken line, and this is what makes the --raw and -// --output text surfaces readable; the JSON envelope path never calls it -// (see outputSlidesXMLGetContent). -// -// Reindentation never enters a textBearingTags element. XML whitespace -// character references are masked before parsing and restored after -// serialization, and CDATA sections are preserved rather than collapsed into -// escaped text. This covers the schema-documented space/tab references as well -// as CR/LF references: serializing a CR reference as a literal line ending -// would make a later XML parse normalize it to LF and break read-modify-write -// round trips. -func prettyPrintXML(xmlContent string) (string, error) { - maskedContent, maskedReferences := maskSMLWhitespaceCharacterReferences(xmlContent) - doc := etree.NewDocument() - doc.ReadSettings.PreserveCData = true - if err := doc.ReadFromString(maskedContent); err != nil { - return "", err - } - if root := doc.Root(); root != nil { - reindentStructural(root, 0) - } - out, err := doc.WriteToString() - if err != nil { - return "", err - } - out = restoreSMLWhitespaceCharacterReferences(out, maskedReferences) - if !strings.HasSuffix(out, "\n") { - out += "\n" - } - return out, nil -} - -// prettyPrintXMLOrOriginal keeps xml-get best-effort: if the server returns -// content that is not strictly valid XML, callers still receive the original -// content and a warning on stderr instead of losing the read path. The bool -// reports whether pretty-printing succeeded, surfaced as pretty_printed in -// --output file metadata. -func prettyPrintXMLOrOriginal(runtime *common.RuntimeContext, xmlContent string) (string, bool) { - out, err := prettyPrintXML(xmlContent) - if err != nil { - fmt.Fprintf(runtime.IO().ErrOut, "warning: XML pretty-print skipped; returning original server content: %v\n", err) - return xmlContent, false - } - return out, true -} - -type maskedXMLCharacterReference struct { - placeholder string - reference string -} - -var numericXMLCharacterReferencePattern = regexp.MustCompile(`&#(?:[0-9]+|x[0-9A-Fa-f]+);`) - -// maskSMLWhitespaceCharacterReferences protects XML whitespace character -// references (space, tab, CR, and LF) from etree's parse/write normalization. -// Each original spelling is restored exactly, including decimal, hexadecimal, -// and zero-padded forms. The placeholder prefix is chosen not to occur in the -// input, so restoring cannot rewrite user-authored content accidentally. -func maskSMLWhitespaceCharacterReferences(xmlContent string) (string, []maskedXMLCharacterReference) { - placeholderPrefix := "LARKCLI_XML_WHITESPACE_REFERENCE_" - for strings.Contains(xmlContent, placeholderPrefix) { - placeholderPrefix += "_" - } - - maskedReferences := make([]maskedXMLCharacterReference, 0) - maskedContent := numericXMLCharacterReferencePattern.ReplaceAllStringFunc(xmlContent, func(reference string) string { - digits := reference[2 : len(reference)-1] - base := 10 - if strings.HasPrefix(digits, "x") { - base = 16 - digits = digits[1:] - } - value, err := strconv.ParseUint(digits, base, 32) - if err != nil { - return reference - } - switch value { - case ' ', '\t', '\r', '\n': - default: - return reference - } - - placeholder := fmt.Sprintf("%s%d_", placeholderPrefix, len(maskedReferences)) - maskedReferences = append(maskedReferences, maskedXMLCharacterReference{ - placeholder: placeholder, - reference: reference, - }) - return placeholder - }) - return maskedContent, maskedReferences -} - -func restoreSMLWhitespaceCharacterReferences(xmlContent string, maskedReferences []maskedXMLCharacterReference) string { - for _, masked := range maskedReferences { - xmlContent = strings.ReplaceAll(xmlContent, masked.placeholder, masked.reference) - } - return xmlContent -} - -// reindentStructural inserts newline+indent whitespace between an element's -// direct children so each nested element sits on its own line, recursing -// only into children that are not textBearingTags. Existing whitespace-only -// CharData between children is treated as pre-existing formatting and -// dropped before reinserting it at the correct depth; non-whitespace CharData -// is never touched, and text-bearing subtrees are never entered at all. -func reindentStructural(e *etree.Element, depth int) { - // A node without element children has no nested structure to reindent. - // Leaving the whole leaf untouched also preserves whitespace-only xs:string - // and simple-content values such as and <chartField>, including - // adjacent plain-text and CDATA nodes. - if textBearingTags[e.Tag] || !hasElementChild(e) { - return - } - - for i := len(e.Child) - 1; i >= 0; i-- { - if cd, ok := e.Child[i].(*etree.CharData); ok && isAllWhitespace(cd.Data) { - e.RemoveChildAt(i) - } - } - if len(e.Child) == 0 { - return - } - - _, lastIsCharData := e.Child[len(e.Child)-1].(*etree.CharData) - childIndent := "\n" + strings.Repeat(" ", depth+1) - closeIndent := "\n" + strings.Repeat(" ", depth) - - for i := len(e.Child) - 1; i >= 0; i-- { - child := e.Child[i] - if ce, ok := child.(*etree.Element); ok { - reindentStructural(ce, depth+1) - } - if _, isCharData := child.(*etree.CharData); !isCharData { - e.InsertChildAt(i, etree.NewCharData(childIndent)) - } - } - if !lastIsCharData { - e.AddChild(etree.NewCharData(closeIndent)) - } -} - -func hasElementChild(e *etree.Element) bool { - for _, child := range e.Child { - if _, ok := child.(*etree.Element); ok { - return true - } - } - return false -} - -// isAllWhitespace reports whether s is non-empty and consists only of XML -// whitespace characters (space, tab, CR, LF). -func isAllWhitespace(s string) bool { - if s == "" { - return false - } - for _, r := range s { - switch r { - case ' ', '\t', '\n', '\r': - default: - return false - } - } - return true -} diff --git a/shortcuts/slides/slides_xml_get_test.go b/shortcuts/slides/slides_xml_get_test.go index 5e8b5be3d..145434db5 100644 --- a/shortcuts/slides/slides_xml_get_test.go +++ b/shortcuts/slides/slides_xml_get_test.go @@ -23,10 +23,6 @@ func TestSlidesXMLGetWritesContentToFileAndSuppressesXML(t *testing.T) { withSlidesTestWorkingDir(t, dir) xml := `<presentation><slide id="s1"><shape id="a">hello</shape></slide></presentation>` - // Golden value computed independently of prettyPrintXML (not derived by - // calling it): a bug in prettyPrintXML itself must not be able to make - // this assertion pass by construction. - wantXML := "<presentation>\n <slide id=\"s1\">\n <shape id=\"a\">hello</shape>\n </slide>\n</presentation>\n" var capturedQuery url.Values f, stdout, _, reg := cmdutil.TestFactory(t, slidesTestConfig(t, "")) reg.Register(&httpmock.Stub{ @@ -64,10 +60,10 @@ func TestSlidesXMLGetWritesContentToFileAndSuppressesXML(t *testing.T) { if err != nil { t.Fatalf("read saved XML: %v", err) } - if string(got) != wantXML { - t.Fatalf("saved XML = %q, want %q", got, wantXML) + if string(got) != xml { + t.Fatalf("saved XML = %q, want %q", got, xml) } - if strings.Contains(stdout.String(), wantXML) { + if strings.Contains(stdout.String(), xml) { t.Fatalf("stdout leaked full XML content: %s", stdout.String()) } if got := capturedQuery.Get("revision_id"); got != "7" { @@ -84,11 +80,8 @@ func TestSlidesXMLGetWritesContentToFileAndSuppressesXML(t *testing.T) { if data["revision_id"] != float64(7) { t.Fatalf("revision_id = %v, want 7", data["revision_id"]) } - if data["pretty_printed"] != true { - t.Fatalf("pretty_printed = %v, want true", data["pretty_printed"]) - } - if data["size"] != float64(len(wantXML)) { - t.Fatalf("size = %v, want %d", data["size"], len(wantXML)) + if data["size"] != float64(len(xml)) { + t.Fatalf("size = %v, want %d", data["size"], len(xml)) } gotPath, _ := data["path"].(string) if !filepath.IsAbs(gotPath) { @@ -103,12 +96,7 @@ func TestSlidesXMLGetReturnsContentEnvelopeWhenOutputOmitted(t *testing.T) { dir := t.TempDir() withSlidesTestWorkingDir(t, dir) - // The JSON envelope carries the server content verbatim: no reindentation - // and no parse/reserialize cycle. Reintroducing the in-repo formatter - // would fail this by inserting indentation; the reference - // additionally guards against a plain unmasked XML round trip, which - // would decode it to a literal space. - xml := `<presentation><slide id="s1"><shape id="a"><content><p><span>Hello</span> <strong>World</strong></p></content></shape></slide></presentation>` + xml := `<presentation><slide id="s1"><shape id="a">hello</shape></slide></presentation>` f, stdout, _, reg := cmdutil.TestFactory(t, slidesTestConfig(t, "")) reg.Register(&httpmock.Stub{ Method: "GET", @@ -134,14 +122,11 @@ func TestSlidesXMLGetReturnsContentEnvelopeWhenOutputOmitted(t *testing.T) { data := decodeShortcutData(t, stdout) presentation := data["xml_presentation"].(map[string]interface{}) if got := presentation["content"]; got != xml { - t.Fatalf("content = %q, want the server content verbatim %q", got, xml) + t.Fatalf("content = %q, want %q", got, xml) } if got := data["xml_presentation_id"]; got != "pres_abc" { t.Fatalf("xml_presentation_id = %v, want pres_abc", got) } - if _, ok := data["pretty_printed"]; ok { - t.Fatalf("pretty_printed should not appear in the envelope: %#v", data) - } if strings.Contains(stdout.String(), "content_saved") { t.Fatalf("stdout should not contain file metadata: %s", stdout.String()) } @@ -151,8 +136,6 @@ func TestSlidesXMLGetJqFiltersContentEnvelopeWhenOutputOmitted(t *testing.T) { dir := t.TempDir() withSlidesTestWorkingDir(t, dir) - // --jq extracts fields from the envelope, and the envelope carries the - // server content verbatim, so the filter yields the single-line original. xml := `<presentation><slide id="s1"><shape id="a">hello</shape></slide></presentation>` f, stdout, _, reg := cmdutil.TestFactory(t, slidesTestConfig(t, "")) reg.Register(&httpmock.Stub{ @@ -178,18 +161,15 @@ func TestSlidesXMLGetJqFiltersContentEnvelopeWhenOutputOmitted(t *testing.T) { t.Fatalf("unexpected error: %v", err) } if got := strings.TrimSpace(stdout.String()); got != xml { - t.Fatalf("stdout = %q, want the server content verbatim %q", got, xml) + t.Fatalf("stdout = %q, want XML content %q", got, xml) } } -func TestSlidesXMLGetPrintsFormattedContentWithoutEnvelopeWhenRaw(t *testing.T) { +func TestSlidesXMLGetPrintsRawContentWhenRaw(t *testing.T) { dir := t.TempDir() withSlidesTestWorkingDir(t, dir) xml := `<presentation><slide id="s1"><shape id="a">hello</shape></slide></presentation>` - // Golden value computed independently of prettyPrintXML; see the comment - // in TestSlidesXMLGetWritesContentToFileAndSuppressesXML. - wantXML := "<presentation>\n <slide id=\"s1\">\n <shape id=\"a\">hello</shape>\n </slide>\n</presentation>\n" f, stdout, _, reg := cmdutil.TestFactory(t, slidesTestConfig(t, "")) reg.Register(&httpmock.Stub{ Method: "GET", @@ -213,32 +193,16 @@ func TestSlidesXMLGetPrintsFormattedContentWithoutEnvelopeWhenRaw(t *testing.T) if err != nil { t.Fatalf("unexpected error: %v", err) } - if got := stdout.String(); got != wantXML { - t.Fatalf("stdout = %q, want formatted XML %q", got, wantXML) + if got := stdout.String(); got != xml { + t.Fatalf("stdout = %q, want raw XML %q", got, xml) } } -func TestSlidesXMLGetRawFlagDocumentsFormattedOutput(t *testing.T) { - for _, flag := range SlidesXMLGet.Flags { - if flag.Name != "raw" { - continue - } - if !strings.Contains(flag.Desc, "formatted XML") || strings.Contains(flag.Desc, "raw XML") { - t.Fatalf("--raw description = %q, want formatted XML without a raw-payload claim", flag.Desc) - } - return - } - t.Fatal("--raw flag not found") -} - func TestSlidesXMLGetFetchesSingleSlideByIDToFile(t *testing.T) { dir := t.TempDir() withSlidesTestWorkingDir(t, dir) xml := `<slide id="slide_1"><data><shape id="a"/></data></slide>` - // Golden value computed independently of prettyPrintXML; see the comment - // in TestSlidesXMLGetWritesContentToFileAndSuppressesXML. - wantXML := "<slide id=\"slide_1\">\n <data>\n <shape id=\"a\"/>\n </data>\n</slide>\n" var capturedQuery url.Values f, stdout, _, reg := cmdutil.TestFactory(t, slidesTestConfig(t, "")) reg.Register(&httpmock.Stub{ @@ -280,8 +244,8 @@ func TestSlidesXMLGetFetchesSingleSlideByIDToFile(t *testing.T) { if err != nil { t.Fatalf("read saved slide XML: %v", err) } - if string(got) != wantXML { - t.Fatalf("saved XML = %q, want %q", got, wantXML) + if string(got) != xml { + t.Fatalf("saved XML = %q, want %q", got, xml) } data := decodeShortcutData(t, stdout) if data["scope"] != "slide" { @@ -299,8 +263,6 @@ func TestSlidesXMLGetFetchesSingleSlideByNumberEnvelope(t *testing.T) { dir := t.TempDir() withSlidesTestWorkingDir(t, dir) - // The slide envelope carries the server content verbatim, like the - // presentation envelope. xml := `<slide id="slide_2"><data><shape id="b"/></data></slide>` var capturedQuery url.Values f, stdout, _, reg := cmdutil.TestFactory(t, slidesTestConfig(t, "")) @@ -343,14 +305,11 @@ func TestSlidesXMLGetFetchesSingleSlideByNumberEnvelope(t *testing.T) { } slide := data["slide"].(map[string]interface{}) if slide["content"] != xml { - t.Fatalf("content = %q, want the server content verbatim %q", slide["content"], xml) + t.Fatalf("content = %q, want %q", slide["content"], xml) } if slide["slide_id"] != "slide_2" { t.Fatalf("slide.slide_id = %v, want slide_2", slide["slide_id"]) } - if _, ok := data["pretty_printed"]; ok { - t.Fatalf("pretty_printed should not appear in the envelope: %#v", data) - } } func TestSlidesXMLGetResolvesWikiPresentation(t *testing.T) { @@ -556,341 +515,3 @@ func TestSlidesXMLGetRejectsRemoveAttrIDForSingleSlide(t *testing.T) { t.Fatalf("param = %q, want --remove-attr-id", validationErr.Param) } } - -func TestPrettyPrintXML(t *testing.T) { - input := `<presentation id="p1" xmlns="http://www.larkoffice.com/sml/2.0" width="960"><slide id="s1"><style><fill id="f1"><fillColor color="rgba(0,0,0,1)"/></fill></style><data/></slide></presentation>` - - got, err := prettyPrintXML(input) - if err != nil { - t.Fatalf("prettyPrintXML: %v", err) - } - if !strings.Contains(got, "\n") { - t.Fatalf("expected reindented output with newlines, got %q", got) - } - if n := strings.Count(got, `xmlns="http://www.larkoffice.com/sml/2.0"`); n != 1 { - t.Fatalf("expected the xmlns declaration to appear exactly once, got %d occurrences in %q", n, got) - } - if !strings.Contains(got, "<data/>") { - t.Fatalf("expected empty <data/> to stay self-closing, got %q", got) - } - if !strings.Contains(got, `<fillColor color="rgba(0,0,0,1)"/>`) { - t.Fatalf("expected attributes to be preserved on their element, got %q", got) - } -} - -func TestPrettyPrintXMLRejectsMalformedInput(t *testing.T) { - if _, err := prettyPrintXML(`<presentation><slide></presentation>`); err == nil { - t.Fatal("expected an error for malformed XML, got nil") - } -} - -// TestPrettyPrintXMLPreservesEscapedWhitespaceReferences covers the schema's -// documented space/tab escape idiom (slides_xml_schema_definition.xml, <p> -// element docs) and CR/LF references whose lexical form is needed to avoid -// XML line-ending normalization on a later parse. etree normally decodes the -// references into literal whitespace. The formatter must preserve their -// lexical representation for safe read-modify-write workflows. -func TestPrettyPrintXMLPreservesEscapedWhitespaceReferences(t *testing.T) { - tests := []struct { - name string - input string - want string - }{ - {"space in p", `<content><p> </p></content>`, "<content>\n <p> </p>\n</content>\n"}, - {"tab in p", `<content><p> </p></content>`, "<content>\n <p> </p>\n</content>\n"}, - {"space in nested span", `<content><p><span> </span></p></content>`, "<content>\n <p><span> </span></p>\n</content>\n"}, - {"hex space", `<content><p> </p></content>`, "<content>\n <p> </p>\n</content>\n"}, - {"zero-padded tab", `<content><p> </p></content>`, "<content>\n <p> </p>\n</content>\n"}, - {"carriage return", `<content><p>A B</p></content>`, "<content>\n <p>A B</p>\n</content>\n"}, - {"line feed", `<content><p>A B</p></content>`, "<content>\n <p>A B</p>\n</content>\n"}, - {"hex carriage return", `<content><p>A B</p></content>`, "<content>\n <p>A B</p>\n</content>\n"}, - {"hex line feed", `<content><p>A B</p></content>`, "<content>\n <p>A B</p>\n</content>\n"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := prettyPrintXML(tt.input) - if err != nil { - t.Fatalf("prettyPrintXML(%q): %v", tt.input, err) - } - if got != tt.want { - t.Fatalf("prettyPrintXML(%q) = %q, want %q", tt.input, got, tt.want) - } - }) - } -} - -func TestPrettyPrintXMLPreservesTextOnlyLeafWhitespace(t *testing.T) { - tests := []struct { - name string - input string - want string - }{ - { - name: "title literal space", - input: `<presentation><title> `, - want: "\n \n \n\n", - }, - { - name: "title escaped space", - input: ` `, - want: "\n \n \n\n", - }, - { - name: "title whitespace CDATA", - input: `<![CDATA[ ]]>`, - want: "\n <![CDATA[ ]]>\n \n\n", - }, - { - name: "chart field literal space", - input: ` `, - want: "\n \n\n", - }, - { - name: "title adjacent text and CDATA", - input: ` <![CDATA[ ]]>`, - want: "\n <![CDATA[ ]]>\n \n\n", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := prettyPrintXML(tt.input) - if err != nil { - t.Fatalf("prettyPrintXML(%q): %v", tt.input, err) - } - if got != tt.want { - t.Fatalf("prettyPrintXML(%q) = %q, want %q", tt.input, got, tt.want) - } - }) - } -} - -// TestPrettyPrintXMLPreservesEscapedSpaceBetweenInlineSiblings is the -// critical case: sitting as a bare sibling text node directly between -// two inline elements, not wrapped in its own tag -- the literal reading of -// the schema's "标签之间...请使用 " guidance, e.g. a plain-styled space -// between two differently formatted words at a pptx run boundary. A fix -// that only special-cases "element whose sole content is whitespace" (such -// as etree's PreserveLeafWhitespace) does not cover this: the whitespace -// here is one of several children of

, not the sole child of . -func TestPrettyPrintXMLPreservesEscapedSpaceBetweenInlineSiblings(t *testing.T) { - input := `

Hello World

` - want := "\n

Hello World

\n
\n" - got, err := prettyPrintXML(input) - if err != nil { - t.Fatalf("prettyPrintXML: %v", err) - } - if got != want { - t.Fatalf("prettyPrintXML(%q) = %q, want %q", input, got, want) - } -} - -func TestPrettyPrintXMLPreservesCDATA(t *testing.T) { - input := `

b & ]]>

` - want := "\n

b & ]]>

\n
\n" - got, err := prettyPrintXML(input) - if err != nil { - t.Fatalf("prettyPrintXML: %v", err) - } - if got != want { - t.Fatalf("prettyPrintXML(%q) = %q, want %q", input, got, want) - } -} - -// TestPrettyPrintXMLSeparatesParagraphsWithoutTouchingTheirText is the -// feature's actual point: a shape with many paragraphs becomes navigable -// (each

on its own indented line), while every paragraph's own rich -// text -- including an inline formatting boundary -- stays byte-for-byte -// unchanged. -func TestPrettyPrintXMLSeparatesParagraphsWithoutTouchingTheirText(t *testing.T) { - input := `

First paragraph.

Second paragraph.

` - want := "\n

First paragraph.

\n

Second paragraph.

\n
\n" - got, err := prettyPrintXML(input) - if err != nil { - t.Fatalf("prettyPrintXML: %v", err) - } - if got != want { - t.Fatalf("prettyPrintXML(%q) = %q, want %q", input, got, want) - } -} - -func TestPrettyPrintXMLIdempotent(t *testing.T) { - input := `

A B C D E