diff --git a/.idea/modules.xml b/.idea/modules.xml index 82e2a9970057e6aa3d9bfb84106056d6c4212d07..e9886ff6a05611ba6e381e8beff9a57843a839a5 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,6 +3,7 @@ <component name="ProjectModuleManager"> <modules> <module fileurl="file://$PROJECT_DIR$/.idea/documentation-manager.iml" filepath="$PROJECT_DIR$/.idea/documentation-manager.iml" /> + <module fileurl="file://$PROJECT_DIR$/../../../schukai/manual/manual.iml" filepath="$PROJECT_DIR$/../../../schukai/manual/manual.iml" /> </modules> </component> </project> \ No newline at end of file diff --git a/application/source/commands/08_document_pdf.go b/application/source/commands/08_document_pdf.go index c660831910be1c3329c56ced33dedff57d201e6c..39efe1610a1d78214daa14af0e1e9fc2524ee8b4 100644 --- a/application/source/commands/08_document_pdf.go +++ b/application/source/commands/08_document_pdf.go @@ -61,4 +61,8 @@ func runDocumentPDF(command *flags.Command) { document.BuildPDF(env) + if environment.State.HasErrorsWarningsOrMessages() { + environment.PrintMessages() + } + } diff --git a/application/source/document/document.go b/application/source/document/document.go index d1d12eba39b2369f1c7a62027145af6a456d55a5..d133be6cc8de577fe5cdcc36afab6fb0f82d1ccb 100644 --- a/application/source/document/document.go +++ b/application/source/document/document.go @@ -21,4 +21,5 @@ type document struct { Created LocaleTime `yaml:"Created"` LastUpdate LocaleTime `yaml:"Last Update"` Language string `yaml:"Language"` + Level int `yaml:"Level"` } diff --git a/application/source/document/files.go b/application/source/document/files.go index 2e3b6f265bafff4739e81fc27f0586da178bab86..511198635a886905dcf1061578d6d97d8a842888 100644 --- a/application/source/document/files.go +++ b/application/source/document/files.go @@ -25,8 +25,26 @@ func (m SourceFileMap) findByRelativePath(rel string) *SourceFile { return nil } +func (m SourceFileMap) findByHash(hash string) *SourceFile { + for _, v := range m { + if v.hash == hash { + return v + break + } + } + return nil +} + func buildFileMap(files []*SourceFile) (SourceFileMap, []string) { + sort.Slice(files, func(i, j int) bool { + if files[i].level == files[j].level { + return files[i].relSourcePath < files[j].relSourcePath + } + + return files[i].level < files[j].level + }) + keys := make([]string, 0, len(files)) mapFiles := make(map[string]*SourceFile) for _, file := range files { @@ -34,8 +52,6 @@ func buildFileMap(files []*SourceFile) (SourceFileMap, []string) { mapFiles[file.absSourcePath] = file } - sort.Strings(keys) - return mapFiles, keys } diff --git a/application/source/document/pdf.go b/application/source/document/pdf.go index c38368045bb35262ec025a8fa0929a4bd6a82f1b..63a53ed83efefb730fa7977c292f59d9353cf8ea 100644 --- a/application/source/document/pdf.go +++ b/application/source/document/pdf.go @@ -3,9 +3,9 @@ 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" - "net/url" "os" "path" "path/filepath" @@ -44,14 +44,21 @@ func NewPdfDataset(env BuildPdfEnvironment) (*PdfDataset, error) { for _, key := range keys { text := mapFiles[key].textMeta.text - text = convertHeadlines(text, mapFiles[key].level) + + 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].hash, mapFiles) + text = replaceRelativeLinks(text, mapFiles[key], mapFiles) + + text = utils.InsertCodeBlocks(text, s2Map) + text = utils.InsertCodeBlocks(text, s1Map) - docs = append(docs, "\\newpage"+text) + docs = append(docs, "\\newpage{}"+text) } d.Documents = strings.Join(docs, "\n") @@ -115,9 +122,9 @@ func BuildPDF(env BuildPdfEnvironment) error { } -func replaceRelativeLinks(content, hash string, fileMap SourceFileMap) string { +func replaceRelativeLinks(content string, f *SourceFile, fileMap SourceFileMap) string { - label := "link_" + hash + label := "link_" + f.hash content = "\\hypertarget{" + label + "}{ } \n" + strings.TrimSpace(content) + "\n" regEx := regexp.MustCompile(`(?:^|[^!])(?P<match>\[(?P<label>[^]]*)\]\((?P<path>[^)]*)\))`) @@ -135,15 +142,32 @@ func replaceRelativeLinks(content, hash string, fileMap SourceFileMap) string { } 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(result["path"]) + 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 + "}{" + result["label"] + "}" + replace := "\\hyperlink{link_" + s.hash + "}{" + escapeLatexSpecialChars(result["label"]) + "}" content = strings.Replace(content, result["match"], replace, -1) } @@ -170,11 +194,6 @@ end } -func isUrl(str string) bool { - u, err := url.Parse(str) - return err == nil && u.Scheme != "" && u.Host != "" -} - type foundedTitleInfoStruct struct { level int match string @@ -183,15 +202,36 @@ type foundedTitleInfoStruct struct { type foundedTitleInfo []foundedTitleInfoStruct -func convertHeadlines(content string, level int) string { +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 content + return info, -1 } - info := foundedTitleInfo{} smallestLevelInTheDoc := 999 for _, match := range matches { @@ -214,18 +254,7 @@ func convertHeadlines(content string, level int) string { } - d := level - 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 + return info, smallestLevelInTheDoc } // https://ftp.gwdg.de/pub/ctan/graphics/awesomebox/awesomebox.pdf @@ -350,7 +379,7 @@ func convertImages(content string, absolute string) string { } } - if isUrl(result["path"]) { + if utils.IsUrl(result["path"]) { continue } diff --git a/application/source/document/yaml.go b/application/source/document/yaml.go index 2c29067274fecbf55ef7c631ca3df7fb3b88eb97..18ff30b4f81e41d9489328a11a9929af7562b08a 100644 --- a/application/source/document/yaml.go +++ b/application/source/document/yaml.go @@ -47,6 +47,10 @@ func evaluateDocumentContent(content []byte) (error, textMetaStruct) { meta := "" text := "" + if strings.Index(origin, "---") == 0 { + origin = "\n" + origin + } + before, remaining, found := strings.Cut(origin, "\n---\n") if !found { diff --git a/application/source/environment/state.go b/application/source/environment/state.go index 2b6d496e85a8099d913b5fcdd64d4cb69a5c5946..71068bca39d3bb5c2d3f9501c0ae651dce283671 100644 --- a/application/source/environment/state.go +++ b/application/source/environment/state.go @@ -33,6 +33,10 @@ func init() { State = &stateStruct{} } +func (e *stateStruct) HasErrorsWarningsOrMessages() bool { + return len(e.errors) > 0 || len(e.warnings) > 0 || len(e.messages) > 0 +} + func (e *stateStruct) SetParser(parser *flags.Parser) *stateStruct { e.parser = parser return e diff --git a/application/source/utils/strings.go b/application/source/utils/strings.go index 8edf317b973efdf2a9cd6908addfde078e1a74c8..0620792c13694ed136516dc0b7fadd931e38f1fd 100644 --- a/application/source/utils/strings.go +++ b/application/source/utils/strings.go @@ -1,6 +1,28 @@ package utils -import "strings" +import ( + "crypto/md5" + "fmt" + "gitlab.schukai.com/oss/utilities/documentation-manager/environment" + "gitlab.schukai.com/oss/utilities/documentation-manager/translations" + "regexp" + "strconv" + "strings" +) + +func Substring(input string, start int, length int) string { + runes := []rune(input) + + if start >= len(runes) { + return "" + } + + if start+length > len(runes) { + length = len(runes) - start + } + + return string(runes[start : start+length]) +} func TrimLines(s string) string { @@ -34,3 +56,59 @@ func TrimQuotes(s string) string { } return s } + +func InsertCodeBlocks(s string, m map[string]string) string { + + for k, v := range m { + s = strings.Replace(s, k, v, -1) + } + + return s + +} + +func MaskCodeBlocks(s, file string, count int) (string, map[string]string) { + + const marker = "__X42_2342XXY_CCX_" + s = strings.Replace(s, "\\`", marker, -1) + + regEx := regexp.MustCompile(`(?m)(?P<match>((?P<open>[\x60]{` + strconv.Itoa(count) + `})(?P<code>[^\x60]+))(?P<close>[\x60]{` + strconv.Itoa(count) + `}))`) + matches := regEx.FindAllStringSubmatch(s, -1) + + m := map[string]string{} + + for _, match := range matches { + result := make(map[string]string) + for i, name := range regEx.SubexpNames() { + if i != 0 && name != "" { + result[name] = match[i] + } + } + + if result["open"] != result["close"] { + environment.State.AddWarning(translations.T.Sprintf("Code block %s, in file %s, is not closed", result["match"], file)) + continue + } + + //i := strings.Index(s, result["match"]) + //if i != -1 { + // q2 := Substring(s, i-20, 40) + // fmt.Println(q2) + // q := Substring(s, i-1, 1) + // if q == "\"" { + // continue + // } + //} + + k := fmt.Sprintf("%x", md5.Sum([]byte(result["code"]))) + m[k] = result["match"] + + s = strings.Replace(s, result["match"], k, -1) + + } + + s = strings.Replace(s, marker, "\\`", -1) + + return s, m + +} diff --git a/application/source/utils/strings_test.go b/application/source/utils/strings_test.go new file mode 100644 index 0000000000000000000000000000000000000000..29772d462b56008defeb80614e4b4ba087b47405 --- /dev/null +++ b/application/source/utils/strings_test.go @@ -0,0 +1,78 @@ +package utils + +import ( + "os" + "path/filepath" + "testing" +) + +func TestMaskCodeBlocks(t *testing.T) { + tables := []struct { + input string + file string + count int + expected int + }{ + {"/development/testing/asstes/code1.md", "code1.md", 3, 9}, + } + + for _, table := range tables { + + p, _ := os.Getwd() + p = filepath.Dir(p) // go to parent directory + p = filepath.Dir(p) // go to parent directory + p = filepath.Dir(p) // go to parent directory + p = filepath.Join(p, table.input) + + code, err := os.ReadFile(p) + if err != nil { + t.Errorf("Error reading file %s", table.input) + } + + s, m := MaskCodeBlocks(string(code), table.file, table.count) + + if len(m) != table.expected { + t.Errorf("Expected %d code blocks, got %d", table.expected, len(m)) + } + + if s == string(code) { + t.Errorf("Expected %s, got %s", string(code), s) + } + + } +} + +func TestSubstring(t *testing.T) { + tables := []struct { + input string + start int + length int + expected string + }{ + {"abcdefg", 0, 0, ""}, + {"abcdefg", 0, 1, "a"}, + {"abcdefg", 0, 2, "ab"}, + {"abcdefg", 0, 3, "abc"}, + {"abcdefg", 0, 4, "abcd"}, + {"abcdefg", 0, 5, "abcde"}, + {"abcdefg", 0, 6, "abcdef"}, + {"abcdefg", 0, 7, "abcdefg"}, + {"abcdefg", 0, 8, "abcdefg"}, + {"abcdefg", 0, 9, "abcdefg"}, + {"abcdefg", 1, 6, "bcdefg"}, + + {"abcdefg", 3, 6, "defg"}, + {"abcdefg", 3, 5, "defg"}, + {"abcdefg", 3, 4, "defg"}, + {"abcdefg", 3, 3, "def"}, + {"abcdefg", 3, 2, "de"}, + {"abcdefg", 3, 1, "d"}, + {"abcdefg", 3, 0, ""}, + } // end of tables + for _, table := range tables { + actual := Substring(table.input, table.start, table.length) + if actual != table.expected { + t.Errorf("Substring(%s, %d, %d) = %s, expected %s", table.input, table.start, table.length, actual, table.expected) + } + } +} diff --git a/application/source/utils/url.go b/application/source/utils/url.go new file mode 100644 index 0000000000000000000000000000000000000000..0ac34852c30c7a466a741c74ebad7900c92e7fc2 --- /dev/null +++ b/application/source/utils/url.go @@ -0,0 +1,8 @@ +package utils + +import "net/url" + +func IsUrl(str string) bool { + u, err := url.Parse(str) + return err == nil && u.Scheme != "" && u.Host != "" +} diff --git a/development/examples/example1/out/my.pdf b/development/examples/example1/out/my.pdf deleted file mode 100644 index 1cae3aa7c5f7bf7c12a43d7de2b2071a0ffd7af9..0000000000000000000000000000000000000000 Binary files a/development/examples/example1/out/my.pdf and /dev/null differ diff --git a/development/testing/asstes/code1.md b/development/testing/asstes/code1.md new file mode 100644 index 0000000000000000000000000000000000000000..a5ffa39edf2481e1433b814c65c40f3d013a68f0 --- /dev/null +++ b/development/testing/asstes/code1.md @@ -0,0 +1,157 @@ +code samples + +Source text can be represented either by indentation or by single quotes. + +If the marking is done with single quotes, the language to be used should be labeled after the +Quotation marks are displayed. + +\`\`\`php +\`\`\`javascript + +When using indentation, the language can be marked with three colons `:::php` + +If no language is selected, the script tries to guess the language. +If no syntax highlighting is to take place, this must be specified explicitly with `nohighlight`. + + +\`\`\`nohighlight + + +Documentation of code examples should always be on executable +code based. For this reason, a program should always be developed independently +will. The integration takes place via this marking:  + +__Example__ + +In the example, the sample file `test.php` is created with the following code. + +```php +<?php +a=0; +a+=1; +echo a; +``` + +If you want to add the first and second line to the documentation and then +the third line, you have to enter the following markings in the file. + +```php +<?php + + EXAMPLE:START + + SNIPPET:PART1:START + +a=0; +a+=1; + + SNIPPET:PART1:END + + SNIPPET:PART2:START + +echo a; + + SNIPPET:PART2:END + + EXAMPLE:END +``` + +The integration into the documentation then takes place as follows: + + + + + ```php + example 1 + ``` + + +and for the second part + + + + + ```php + example2 + ``` + +The __result__ is then: + + a=0; + a+=1; + +and + + echo a; + +tabs + +Tabs can be used to integrate examples for different languages into a code block. + + +``` bash tab="Bash" +!binbash + +echo "Hello world!" +``` + +```c tab="C" +include <stdio.h> + +int main(void) { + printf("Hello world!\n"); +} +``` + +``` c++ tab="C++" +include <iostream> + +int main() { + std::cout << "Hello world!" << std::endl; + return 0; +} +``` + +```c tab="C" +using System; + +class Program { + static void Main(string[] args) { + Console. WriteLine("Hello world!"); + } +} +``` + +``` bash tab="Bash" +!binbash + +echo "Hello world!" +``` + +```c tab="C" +include <stdio.h> + +int main(void) { + printf("Hello world!\n"); +} +``` + +``` c++ tab="C++" +include <iostream> + +int main() { + std::cout << "Hello world!" << std::endl; + return 0; +} +``` + +```c tab="C" +using System; + +class Program { + static void Main(string[] args) { + Console. WriteLine("Hello world!"); + } +} + +``` \ No newline at end of file