package document import ( "fmt" "gitlab.schukai.com/oss/utilities/documentation-manager/environment" "gitlab.schukai.com/oss/utilities/documentation-manager/translations" "gitlab.schukai.com/oss/utilities/documentation-manager/utils" "io/ioutil" "os" "path" "path/filepath" "regexp" "strings" "text/template" ) type BuildPdfEnvironment struct { SourcePath string DateFormat string OutputPath string Verbose bool Templates struct { Latex string Markdown string } } type PdfDataset struct { Documents string } func NewPdfDataset(env BuildPdfEnvironment) (*PdfDataset, error) { files, err := getFiles(env.SourcePath) if err != nil { return nil, err } mapFiles, keys := buildFileMap(files) d := &PdfDataset{} docs := []string{} for _, key := range keys { text := mapFiles[key].textMeta.text text, s1Map := utils.MaskCodeBlocks(text, mapFiles[key].relSourcePath, 3) text, s2Map := utils.MaskCodeBlocks(text, mapFiles[key].relSourcePath, 1) text = convertHeadlines(text, mapFiles[key].level, mapFiles[key].textMeta.meta.Level) text = convertAwesomeBoxes(text) text = convertImages(text, mapFiles[key].baseDir) text = convertCircledNumbers(text) text = replaceKbd(text) text = replaceRelativeLinks(text, mapFiles[key], mapFiles) text = utils.InsertCodeBlocks(text, s2Map) text = utils.InsertCodeBlocks(text, s1Map) docs = append(docs, "\\newpage{}"+text) } d.Documents = strings.Join(docs, "\n") if env.Verbose { fmt.Println(d.Documents) } return d, nil } func BuildPDF(env BuildPdfEnvironment) error { output := env.OutputPath if env.DateFormat != "" { dateFormat = env.DateFormat } if !filepath.IsAbs(output) { pwd := os.Getenv("PWD") output = path.Clean(path.Join(pwd, output)) } if output == "" { environment.ExitWithError(2, "if the type is pdf, the output option must be specified") } file, err := ioutil.TempFile(os.TempDir(), environment.State.GetInfo().Mnemonic) checkError(err) defer func() { file.Close() os.Remove(file.Name()) }() content := environment.ReadTemplate(env.Templates.Markdown) t, err := template.New("pdf").Parse(content) checkError(err) d, err := NewPdfDataset(env) checkError(err) err = t.Execute(file, d) checkError(err) luaFilter := createLuaFile() defer func() { if luaFilter != nil { luaFilter.Close() os.Remove(luaFilter.Name()) } }() c, _ := os.ReadFile(file.Name()) os.WriteFile("/tmp/debug.txt", c, 0644) runPandoc(file.Name(), output, env.Templates.Latex, luaFilter.Name(), env.Verbose) return nil } func replaceRelativeLinks(content string, f *SourceFile, fileMap SourceFileMap) string { label := "link_" + f.hash content = "\\hypertarget{" + label + "}{ } \n" + strings.TrimSpace(content) + "\n" regEx := regexp.MustCompile(`(?:^|[^!])(?P<match>\[(?P<label>[^]]*)\]\((?P<path>[^)]*)\))`) matches := regEx.FindAllStringSubmatch(content, -1) if matches == nil { return content } for _, match := range matches { result := make(map[string]string) for i, name := range regEx.SubexpNames() { if i != 0 && name != "" { result[name] = match[i] } } if filepath.IsAbs(result["path"]) { //environment.State.AddWarning(translations.T.Sprintf("Absolute path not found: %s", result["path"])) continue } if utils.IsUrl(result["path"]) { continue } d := filepath.Dir(f.relSourcePath) p := filepath.Join(d, result["path"]) p = strings.Split(p, "#")[0] ext := filepath.Ext(p) if ext != ".md" && ext != ".markdown" { environment.State.AddWarning(translations.T.Sprintf("file extension %s, in file %s, not supported for relative links", ext, f.absSourcePath)) continue } s := fileMap.findByRelativePath(p) if s == nil { environment.State.AddWarning(translations.T.Sprintf("relative path %s, in file %s, cannot be resolved", result["path"], f.absSourcePath)) continue } replace := "\\hyperlink{link_" + s.hash + "}{" + escapeLatexSpecialChars(result["label"]) + "}" content = strings.Replace(content, result["match"], replace, -1) } return content } func createLuaFile() *os.File { tmp, err := ioutil.TempFile(os.TempDir(), "lua-filter") if err != nil { environment.ExitWithError(2, "A temporary file cannot be created", err.Error()) } tmp.WriteString(` function RawBlock (raw) return raw.format:match "html" and pandoc.read(raw.text, "html").blocks or raw end `) return tmp } type foundedTitleInfoStruct struct { level int match string title string } type foundedTitleInfo []foundedTitleInfoStruct func convertHeadlines(content string, level, startLevel int) string { d := 0 info, smallestLevelInTheDoc := calculateLevel(content) if startLevel == 0 { d = level - smallestLevelInTheDoc } else { d = startLevel - smallestLevelInTheDoc } if d < 0 { d = 0 } for _, i := range info { fill := strings.Repeat("#", d+i.level) nw := fill + " " + i.title + "\n" content = strings.Replace(content, i.match, nw, 1) } return content } func calculateLevel(content string) (foundedTitleInfo, int) { regEx := regexp.MustCompile(`(?m)^(?P<match>(?P<level>#+)\s+(?P<title>[^\n]*))`) matches := regEx.FindAllStringSubmatch(content, -1) info := foundedTitleInfo{} if matches == nil { return info, -1 } smallestLevelInTheDoc := 999 for _, match := range matches { result := make(map[string]string) for i, name := range regEx.SubexpNames() { if i != 0 && name != "" { result[name] = match[i] } } info = append(info, foundedTitleInfoStruct{ level: len(result["level"]), match: result["match"], title: result["title"], }) if len(result["level"]) < smallestLevelInTheDoc { smallestLevelInTheDoc = len(result["level"]) } } return info, smallestLevelInTheDoc } // https://ftp.gwdg.de/pub/ctan/graphics/awesomebox/awesomebox.pdf func convertAwesomeBoxes(content string) string { regEx := regexp.MustCompile(`(?m)(?P<matches>!!!\s*(?P<type>[^\s]+)\s+?(?P<title>[^\n]*)\n(?P<lines>(?P<lastline>[[:blank:]]+[^\n]+\n)+))`) matches := regEx.FindAllStringSubmatch(content, -1) if matches == nil { return content } for _, match := range matches { result := make(map[string]string) for i, name := range regEx.SubexpNames() { if i != 0 && name != "" { result[name] = match[i] } } boxtype := "note" switch { case utils.Contains([]string{"notebox", "note", "info"}, result["type"]): boxtype = "note" case utils.Contains([]string{"tipbox", "tip", "hint"}, result["type"]): boxtype = "tip" case utils.Contains([]string{"warningbox", "warning", "warn"}, result["type"]): boxtype = "warning" case utils.Contains([]string{"cautionbox", "caution", "danger"}, result["type"]): boxtype = "caution" case utils.Contains([]string{"importantbox", "important"}, result["type"]): boxtype = "important" } c := "" t := escapeLatexSpecialChars(result["title"]) if t != "" { c += "\\textbf{" + utils.TrimQuotes(t) + "}\n" } lines := result["lines"] lines = convertAwesomeBoxesMarkdown(lines) c += "\n" + lines + "\n" //+escapeLatexSpecialChars(result["lastline"])) + "\n" awesomebox := `\begin{` + boxtype + `block}` + c + "\n" + `\end{` + boxtype + `block}` content = strings.Replace(content, result["matches"], "\n"+awesomebox+"\n", 1) } return content } func convertAwesomeBoxesMarkdown(content string) string { output := runInlinePandoc(escapeLatexSpecialChars(utils.TrimLines(content))) return output } // The following characters play a special role in LaTeX and are called special printing characters, or simply special characters. // // # $ % & ~ _ ^ \ { } // // Whenever you put one of these special characters into your file, you are doing something special. If you simply want the character // to be printed just as any other letter, include a \ in front of the character. For example, \$ will produce $ in your output. // // The exception to the rule is the \ itself because \\ has its own special meaning. A \ is produced by typing $\backslash$ in your file. // // The meaning of these characters are: // // ~ (tilde) unbreakable space, use it whenever you want to leave a space which is unbreakable, and cannot expand or shrink, as e.q. in names: A.~U.~Thor. // $ (dollar sign) to start an finish math mode. // _ (underscore) for subscripts in math mode. // ^ (hat) for superscripts in math mode. // \ (backslash) starting commands, which extend until the first non-alphanumerical character. The space following the command is swallowed. The following line results in what expected: // The \TeX nician is an expert in \TeX{} language. // {} (curly brackets) to group and separate commands from its surroundings. Must appear in pairs. func escapeLatexSpecialChars(content string) string { result := "" runes := []rune(content) for i, k := range runes { if k == '\\' { result += "\\textbackslash{}" continue } else if utils.Contains([]string{"#", "$", "%", "&", "~", "_", "^", "\\", "{", "}"}, string(k)) { if i == 0 || runes[i-1] != '\\' { result += "\\" } } result += string(k) } return result } func convertImages(content string, absolute string) string { regEx := regexp.MustCompile(`(?P<match>\!\[(?P<label>[^]]*)\]\((?P<path>[^)]*)\))`) matches := regEx.FindAllStringSubmatch(content, -1) if matches == nil { return content } for _, match := range matches { result := make(map[string]string) for i, name := range regEx.SubexpNames() { if i != 0 && name != "" { result[name] = match[i] } } if utils.IsUrl(result["path"]) { continue } if filepath.IsAbs(result["path"]) { continue } p := filepath.Join(absolute, result["path"]) path := path.Clean(p) content = strings.Replace(content, result["match"], "!["+result["label"]+"]("+path+")", -1) } return content } //func convertTemplateLogo(content string, absolute string) string { // todoRegEx := regexp.MustCompile(`(?m)^(?P<match>logo:\s*"?(?P<path>[^"\n]*)"?\s*)$`) // // matches := todoRegEx.FindAllStringSubmatch(content, -1) // if matches == nil { // return content // } // // for _, match := range matches { // result := make(map[string]string) // for i, name := range todoRegEx.SubexpNames() { // if i != 0 && name != "" { // result[name] = match[i] // } // } // // if filepath.IsAbs(result["path"]) { // continue // } // // path := path.Clean(absolute + "/" + result["path"]) // content = strings.Replace(content, result["match"], "logo: \""+path+"\"", -1) // // } // // return content //} //func convertTemplateLatexLogo(content string, absolute string) string { // todoRegEx := regexp.MustCompile(`(?m)(?P<match>\\includegraphics[^{]*\{(?P<path>[^}]*)\})`) // // matches := todoRegEx.FindAllStringSubmatch(content, -1) // if matches == nil { // return content // } // // for _, match := range matches { // result := make(map[string]string) // for i, name := range todoRegEx.SubexpNames() { // if i != 0 && name != "" { // result[name] = match[i] // } // } // // if filepath.IsAbs(result["path"]) { // continue // } // // path := path.Clean(absolute + "/" + result["path"]) // a := strings.Replace(result["match"], result["path"], path, -1) // // content = strings.Replace(content, result["match"], a, -1) // // } // // return content //} // //func convertTemplateImages(content string, absolute string) string { // content = convertTemplateLogo(content, absolute) // content = convertTemplateLatexLogo(content, absolute) // // return content // //}