package jdr import ( "encoding/xml" "html/template" "errors" "log" "os" ) // Page represents a page of a character sheet. type Page struct { Items []SheetItem } // ToHTML transforms a Page into a HTML code. // This HTML code is embeddable into the character sheet webpage func (page *Page) ToHTML() template.HTML { ret := "" for _, item := range page.Items { ret += string(item.ToHTML()) } return template.HTML(ret) } // SheetItem represents an item in the character sheet (either a structural element or a value) type SheetItem interface { Name() string ToHTML() template.HTML } // Block represents a SheetItem that contains other SheetItems. // It is used as a structural element to group values together. type Block struct { name string Items []SheetItem } // Name of the block (possibly empty if the block is unnamed) func (block *Block) Name() string { return block.name } // ToHTML transforms a Block into a HTML code. func (block *Block) ToHTML() template.HTML { begin := `
` legend := `` + template.HTMLEscapeString(block.Name()) + `` end := `
` ret := template.HTML(begin) if block.Name() != "" { ret += template.HTML(legend) } for _, item := range block.Items { ret += item.ToHTML() } ret += template.HTML(end) return ret } // Variable is a value in the character sheet, such as the name of the character, or its XP count type Variable struct { Type VariableType name string } // Name of the variable func (v *Variable) Name() string { return v.name } // ToHTML transforms a Variable into a HTML code func (v *Variable) ToHTML() template.HTML { label := "" if v.name != "" { label = `` } html, err := v.Type.ToHTML(v.Type.DefaultValue()) if err != nil { log.Fatal(err) } return template.HTML(label) + html + template.HTML(`
`) } // CharacterSheet is a role-play character sheet. // It is structured in Pages, Blocks and Variables type CharacterSheet struct { types map[string]VariableType Pages []*Page } // Script renders the Javascript code necessary to render the character sheet web page interactive func (cs *CharacterSheet) Script() template.JS { return template.JS("") } // Render the CharacterSheet into an embeddable HTML code func (cs *CharacterSheet) Render() template.HTML { ret := template.HTML("") for _, page := range cs.Pages { ret += page.ToHTML() } return ret } // AddType adds a custom variable type to the character sheet func (cs *CharacterSheet) AddType(t VariableType) { //create the type array if it doesn't exist yet if cs.types == nil { cs.types = make(map[string]VariableType) } name := t.Name() cs.types[name] = t } // AddDefaultTypes adds the default variable types to the character sheet func (cs *CharacterSheet) AddDefaultTypes() { cs.AddType(&VariableType_bool{}) cs.AddType(&VariableType_int{}) cs.AddType(&VariableType_float{}) cs.AddType(&VariableType_string{}) cs.AddType(&VariableType_d{}) } // getType searches into the types (default & customs) of a character sheet for one matching "typename" func (cs *CharacterSheet) getType(typename string) (VariableType, error) { t, ok := cs.types[typename] if !ok { return &VariableType_badType{}, errors.New("unknown type \"" + typename + "\"") } return t, nil } // xmlNode is a temporary structure used for parsing the character sheet XML file // without knowing its structure à priori type xmlNode struct { XMLName xml.Name Attrs []xml.Attr `xml:"-"` Content []byte `xml:",innerxml"` Nodes []xmlNode `xml:",any"` } func (n *xmlNode) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { n.Attrs = start.Attr type node xmlNode return d.DecodeElement((*node)(n), &start) } // The parseVariable function parses a Variable value from a XML node. // The variable type is inferred from the "type" attribute of the node. // If this attribute is omitted, the type of the variable must be given through the // "defaults" parameter to avoid a parsing error. // If this attribute is specified, the "defaults" parameter will be unused and may be nil func parseVariable(root xmlNode, sheet *CharacterSheet, defaults *Variable) (*Variable, error) { ret := &Variable{} typeOk := false nameOk := false //parsing default values if defaults != nil { switch defaults.Type.(type) { case *VariableType_badType: typeOk = false default: typeOk = true ret.Type = defaults.Type } } //iterating on the attributes for i := 0; i < len(root.Attrs); i++ { attrName := root.Attrs[i].Name.Local attrVal := root.Attrs[i].Value switch attrName { case "type": t, err := sheet.getType(attrVal) if err != nil { return nil, err } ret.Type = t typeOk = true case "name": ret.name = attrVal nameOk = true } } //check if no parse error if !nameOk { return nil, errors.New("Unnamed variable") } if !typeOk { return nil, errors.New("No type provided for variable \"" + ret.Name() + "\"") } return ret, nil } func parseBlock(root xmlNode, sheet *CharacterSheet) (*Block, error) { ret := &Block{} for i := 0; i < len(root.Attrs); i++ { attrName := root.Attrs[i].Name.Local attrVal := root.Attrs[i].Value switch attrName { case "name": ret.name = attrVal } } for i := 0; i < len(root.Nodes); i++ { items, err := parseItem(root.Nodes[i], sheet) if err != nil { return nil, err } for _, item := range items { ret.Items = append(ret.Items, item) } } return ret, nil } func parseVariableGroup(root xmlNode, sheet *CharacterSheet, defaults *Variable) ([]*Variable, error) { ret := []*Variable{} typeOk := false if defaults == nil { defaults = &Variable{} } else { typeOk = true } for i := 0; i < len(root.Attrs); i++ { attrName := root.Attrs[i].Name.Local attrVal := root.Attrs[i].Value switch attrName { case "default-type": t, err := sheet.getType(attrVal) if err != nil { return nil, err } defaults.Type = t typeOk = true } } if !typeOk { defaults.Type = &VariableType_badType{} } for i := 0; i < len(root.Nodes); i++ { node := root.Nodes[i] switch node.XMLName.Local { case "variable": item, err := parseVariable(node, sheet, defaults) if err != nil { return nil, err } ret = append(ret, item) case "variable-group": items, err := parseVariableGroup(node, sheet, defaults) if err != nil { return nil, err } for j := 0; j < len(items); j++ { ret = append(ret, items[j]) } default: return nil, errors.New("Unknown variable-group item: \"" + node.XMLName.Local + "\"") } } return ret, nil } func parseItem(root xmlNode, sheet *CharacterSheet) ([]SheetItem, error) { switch root.XMLName.Local { case "block": block, err := parseBlock(root, sheet) if err != nil { return nil, err } return []SheetItem{block}, nil case "variable": variable, err := parseVariable(root, sheet, nil) if err != nil { return nil, err } return []SheetItem{variable}, err case "variable-group": variables, err := parseVariableGroup(root, sheet, nil) if err != nil { return nil, err } items := make([]SheetItem, len(variables)) for i, v := range variables { items[i] = SheetItem(v) } return items, nil default: return nil, errors.New("Unknown item: " + root.XMLName.Local) } } func parsePage(root xmlNode, sheet *CharacterSheet) (*Page, error) { ret := &Page{} for i := 0; i < len(root.Nodes); i++ { items, err := parseItem(root.Nodes[i], sheet) if err != nil { return nil, err } for _, item := range items { ret.Items = append(ret.Items, item) } } return ret, nil } func parseSheet(root xmlNode) (*CharacterSheet, error) { if root.XMLName.Local != "jdr-desc" { return nil, errors.New("Wrong root node") } sheet := &CharacterSheet{} sheet.AddDefaultTypes() for i := 0; i < len(root.Nodes); i++ { node := root.Nodes[i] switch node.XMLName.Local { case "custom-types": err := parseCustomTypes(node, sheet) if err != nil { return nil, err } case "page": page, err := parsePage(node, sheet) if err != nil { return nil, err } sheet.Pages = append(sheet.Pages, page) default: return nil, errors.New("Wrong format") } } return sheet, nil } // ReadCharacterSheet opens a character sheet XML file // and parses the XML to get a CharacterSheet struct func ReadCharacterSheet(filename string) (*CharacterSheet, error) { file, err := os.Open(filename) if err != nil { return nil, err } defer file.Close() decoder := xml.NewDecoder(file) var n xmlNode err = decoder.Decode(&n) if err != nil { return nil, err } sheet, err := parseSheet(n) if err != nil { return nil, err } return sheet, nil }