package d2svg

import (
	
	
	
	
	
)

// fontCorpora contains the characters rendered by each embedded font class.
// Absence means the face is unused. A nil map asks the embedding code to keep
// its legacy source triggers and whole-diagram corpus instead.
type fontCorpora map[string]string

// BaseStylesheet is public and callers may add font-selection rules. Keep the
// original text so those callers retain whole-corpus font subsets.
var fontCorpusBaseStylesheet = BaseStylesheet

// restrictFontCorpora narrows the freshly collected map in place only when its
// characters appeared in the legacy corpus. Rendering can introduce characters
// absent from that corpus (notably code's nonbreaking spaces). Their browser
// fallback can use other legacy glyphs, such as ordinary spaces, so preserve the
// complete legacy subset for that face instead of changing those dependencies.
// Unicode normalization and bidirectional shaping can also use characters from
// other runs or faces. Keep all legacy faces and their corpora for non-ASCII
// input, and for controls whose whitespace aliases are browser-dependent.
func restrictFontCorpora( fontCorpora,  string) fontCorpora {
	if  == nil {
		return nil
	}
	var  [128]bool
	for  := 0;  < len(); ++ {
		 := []
		if  >= 128 ||  == 127 ||  < 32 &&  != '\n' {
			return nil
		}
		[] = true
	}
	for ,  := range  {
		for ,  := range  {
			if  >= 128 || ![] {
				[] = 
				break
			}
		}
	}
	return 
}

func appendFontOnTrigger( *bytes.Buffer,  string,  []string,  fontCorpora, ,  string,  func(string) string) {
	if  == nil {
		appendOnTriggerLazy(, , , func() string { return () })
	} else if ,  := [];  {
		.WriteString(())
	}
}

// This order is the cascade order of the font-family rules in EmbedFonts.
// In particular, matching text-mono-bold must not also match text-mono.
var embeddedFontClasses = [...]string{
	"text", "text-semibold", "text-bold", "text-italic",
	"text-mono", "text-mono-semibold", "text-mono-bold", "text-mono-italic",
}

// collectFontCorpora is for the body fragment produced by Render, before its
// stylesheets are added. It follows SVG text inheritance, including native
// Markdown runs and syntax-highlighted tspans. It is deliberately not a CSS or
// HTML evaluator: unsupported font selection, referenced/dynamic text, and
// malformed fragments request the existing full-corpus embedding path.
func collectFontCorpora( string) (fontCorpora, bool) {
	, ,  := collectFontCorporaAndClasses()
	return , 
}

// collectFontCorporaAndClasses also records palette classes during the text
// scan. A nonnil class map means the complete fragment passed the existing
// theme parser's safety checks and can avoid a second XML pass.
func collectFontCorporaAndClasses( string) (fontCorpora, map[string]bool, bool) {
	if BaseStylesheet != fontCorpusBaseStylesheet {
		return nil, nil, false
	}
	 := xml.NewDecoder(io.MultiReader(
		strings.NewReader(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">`),
		strings.NewReader(), strings.NewReader(`</svg>`),
	))
	type  struct {
		   int
		   bool
		 bool
	}
	 := []{{: -1}}
	var  [len(embeddedFontClasses)]strings.Builder
	 := make(map[string]bool)
	 := true
	for {
		,  := .Token()
		if  == io.EOF {
			break
		}
		if  != nil {
			return nil, nil, false
		}
		switch token := .(type) {
		case xml.StartElement:
			if strings.EqualFold(.Name.Local, "script") {
				 = false
			}
			if .Name.Space != "" && .Name.Space != "http://www.w3.org/2000/svg" {
				return nil, nil, false
			}
			 := [len()-1]
			switch .Name.Local {
			case "style", "script", "foreignObject", "use", "tref", "textPath",
				"animate", "animateMotion", "animateTransform", "set":
				return nil, nil, false
			case "title", "desc", "metadata":
				. = true
			case "text":
				. = true
			case "tspan", "a":
			default:
				if . && !. {
					return nil, nil, false
				}
			}

			// A class on the child overrides an inherited family, even if the
			// parent's class occurs later in the stylesheet. Only classes on
			// the same element compete according to stylesheet order.
			 := -1
			 := false
			for ,  := range .Attr {
				// Match pruneThemeCSS's guards independently of the stricter
				// font guards, so neither specialization changes the other.
				 := strings.ToLower(.Name.Local)
				if strings.HasPrefix(, "on") ||  == "attributename" && strings.EqualFold(strings.TrimSpace(.Value), "class") {
					 = false
				}
				if  == "href" {
					 := strings.TrimSpace(.Value)
					if strings.EqualFold(.Name.Local, "use") && !strings.HasPrefix(, "#") || strings.HasPrefix(strings.ToLower(), "javascript:") {
						 = false
					}
				}
				if strings.HasPrefix(.Name.Local, "font-variant") {
					return nil, nil, false
				}
				switch .Name.Local {
				case "class":
					if .Name.Space != "" ||  {
						return nil, nil, false
					}
					 = true
					for ,  := range strings.FieldsFunc(.Value, func( rune) bool {
						// The separate theme parser uses strings.Fields, which
						// also splits Unicode whitespace. Preserve that path
						// for custom classes with different tokenization.
						if  >= 128 && unicode.IsSpace() {
							 = false
						}
						return  == ' ' ||  == '\t' ||  == '\n' ||  == '\r' ||  == '\f'
					}) {
						if isPaletteClass() {
							[] = true
						}
						for ,  := range embeddedFontClasses {
							if  ==  &&  >  {
								 = 
							}
						}
					}
				case "font-family", "text-transform", "direction", "unicode-bidi":
					return nil, nil, false
				case "style":
					if .Name.Space != "" || !fontCorpusStyleSafe(.Value) {
						return nil, nil, false
					}
				default:
					if strings.HasPrefix(.Name.Local, "on") {
						return nil, nil, false
					}
				}
			}
			if  >= 0 {
				. = 
			}
			 = append(, )
		case xml.EndElement:
			if len() == 1 {
				return nil, nil, false
			}
			 = [:len()-1]
		case xml.CharData:
			 := [len()-1]
			if . && !. && len() != 0 {
				if . < 0 {
					return nil, nil, false
				}
				// Keep whitespace too: code uses nonbreaking spaces, and text
				// layout can preserve ordinary spaces with xml:space.
				[.].Write()
			}
		case xml.Directive, xml.ProcInst:
			return nil, nil, false
		}
	}
	if len() != 1 {
		return nil, nil, false
	}
	 := make(fontCorpora)
	for  := range  {
		if [].Len() != 0 {
			[embeddedFontClasses[]] = [].String()
		}
	}
	// The legacy regular-face trigger is narrower than CSS class matching.
	// Customized SVG fragments may use another class first, or single quotes;
	// do not introduce a regular-family rule that the old output lacked.
	if ,  := ["text"];  &&
		!strings.Contains(, `class="text"`) && !strings.Contains(, `class="text `) &&
		!strings.Contains(, `class="md"`) && !strings.Contains(, `class="md `) {
		return nil, nil, false
	}
	if ! {
		 = nil
	}
	return , , true
}

// Generated inline styles use simple declarations. Reject CSS syntax that
// could disguise a font-family override or change the rendered characters.
// Font weight/style alone synthesize against the selected family; the D2
// @font-face declarations do not declare separate weight/style descriptors.
func fontCorpusStyleSafe( string) bool {
	if strings.ContainsAny(, `\/{}`) {
		return false
	}
	for ,  := range strings.Split(, ";") {
		if strings.TrimSpace() == "" {
			continue
		}
		, ,  := strings.Cut(, ":")
		if ! {
			return false
		}
		 = strings.ToLower(strings.TrimSpace())
		if strings.HasPrefix(, "font-variant") {
			return false
		}
		switch  {
		case "font", "font-family", "text-transform", "direction", "unicode-bidi", "all", "content":
			return false
		}
	}
	return true
}