@@ -1,746 +0,0 @@
package main
import (
"bytes"
"flag"
"fmt"
"html"
"io/fs"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
type ParamSpec struct {
ID int ` yaml:"id" `
Code string ` yaml:"code" `
DataType string ` yaml:"data_type" `
Type string ` yaml:"type" `
Required bool ` yaml:"required" `
Default interface { } ` yaml:"default" `
Descr string ` yaml:"descr" `
Man string ` yaml:"man" `
RefSvcID * int ` yaml:"ref_svc_id" `
Func string ` yaml:"func" `
MinValue * int ` yaml:"minvalue" `
MaxValue * int ` yaml:"maxvalue" `
Regex string ` yaml:"regex" `
ValueList [ ] string ` yaml:"value_list" `
Unique string ` yaml:"unique_scope" `
MaxLength * int ` yaml:"maxlength" `
MinLength * int ` yaml:"minlength" `
}
type OperationSpec struct {
Name string ` yaml:"name" `
ID int ` yaml:"id" `
Kind string ` yaml:"kind" `
Action string ` yaml:"action" `
Subresource string ` yaml:"subresource" `
Man string ` yaml:"man" `
Params [ ] ParamSpec ` yaml:"params" `
}
type OutputParam struct {
Code string ` yaml:"code" `
Type string ` yaml:"type" `
Sensitive bool ` yaml:"sensitive" `
}
type ServiceSpec struct {
Name string ` yaml:"name" `
ServiceID int ` yaml:"service_id" `
ServiceDisplayName string ` yaml:"service_display_name" `
ServiceMan string ` yaml:"service_man" `
Lifecycle struct {
DeleteModeDefault string ` yaml:"delete_mode_default" `
ResumeIfExistsDefault bool ` yaml:"resume_if_exists_default" `
} ` yaml:"lifecycle" `
Outputs struct {
Params [ ] OutputParam ` yaml:"params" `
} ` yaml:"outputs" `
Operations [ ] OperationSpec ` yaml:"operations" `
}
type ServiceMeta struct {
ID int
Name string
}
func main ( ) {
resourcesDirFlag := flag . String ( "resources" , "" , "Path to resources_yaml" )
docsDirFlag := flag . String ( "docs" , "" , "Path to docs/30_registry/resources" )
servicesListFlag := flag . String ( "services" , "" , "Path to devops/config/services_list.txt" )
excludeFlag := flag . String ( "exclude" , "clickhouse" , "Comma-separated resource names to skip" )
versionFlag := flag . String ( "version" , "" , "Provider version for example block" )
apiEndpointFlag := flag . String ( "api-endpoint" , "https://deck-api.ngcloud.ru/api/v1/index.cfm" , "API endpoint for example block" )
flag . Parse ( )
root := detectRoot ( )
resourcesDir := pickPath ( * resourcesDirFlag , filepath . Join ( root , "universal_rebuild" , "resources_yaml" ) )
docsDir := pickPath ( * docsDirFlag , filepath . Join ( root , "docs" , "30_registry" , "resources" ) )
servicesList := pickPath ( * servicesListFlag , filepath . Join ( root , "devops" , "config" , "services_list.txt" ) )
excludeSet := toSet ( * excludeFlag )
version := * versionFlag
if version == "" {
version = detectVersion ( filepath . Join ( root , "universal_rebuild" , "main.go" ) )
}
apiEndpoint := * apiEndpointFlag
servicesOrder := loadServicesList ( servicesList )
specs := loadSpecs ( resourcesDir , servicesOrder )
if len ( specs ) == 0 {
fmt . Fprintf ( os . Stderr , "No specs found in %s\n" , resourcesDir )
os . Exit ( 1 )
}
if err := os . MkdirAll ( docsDir , 0 o755 ) ; err != nil {
panic ( err )
}
for _ , spec := range specs {
if excludeSet [ spec . Name ] {
continue
}
writeResourceDocs ( docsDir , spec , version , apiEndpoint )
}
}
func detectRoot ( ) string {
cwd , err := os . Getwd ( )
if err != nil {
panic ( err )
}
if filepath . Base ( cwd ) == "universal_rebuild" {
return filepath . Dir ( cwd )
}
return cwd
}
func pickPath ( value , fallback string ) string {
if value != "" {
return value
}
return fallback
}
func toSet ( csv string ) map [ string ] bool {
out := map [ string ] bool { }
for _ , item := range strings . Split ( csv , "," ) {
trimmed := strings . TrimSpace ( item )
if trimmed == "" {
continue
}
out [ trimmed ] = true
}
return out
}
func detectVersion ( mainPath string ) string {
b , err := os . ReadFile ( mainPath )
if err != nil {
return "2.x"
}
re := regexp . MustCompile ( ` version string\s*=\s*"([0-9.]+)" ` )
match := re . FindStringSubmatch ( string ( b ) )
if len ( match ) > 1 {
return match [ 1 ]
}
return "2.x"
}
func loadServicesList ( path string ) [ ] ServiceMeta {
b , err := os . ReadFile ( path )
if err != nil {
return nil
}
lines := strings . Split ( string ( b ) , "\n" )
out := [ ] ServiceMeta { }
for _ , line := range lines {
line = strings . TrimSpace ( line )
if line == "" || strings . HasPrefix ( line , "#" ) {
continue
}
head := strings . SplitN ( line , "#" , 2 ) [ 0 ]
parts := strings . Fields ( head )
if len ( parts ) < 2 {
continue
}
id , err := strconv . Atoi ( parts [ 0 ] )
if err != nil {
continue
}
out = append ( out , ServiceMeta { ID : id , Name : parts [ 1 ] } )
}
return out
}
func loadSpecs ( dir string , ordered [ ] ServiceMeta ) [ ] ServiceSpec {
specsByID := map [ int ] ServiceSpec { }
walkErr := filepath . WalkDir ( dir , func ( path string , d fs . DirEntry , err error ) error {
if err != nil {
return err
}
if d . IsDir ( ) || ! strings . HasSuffix ( d . Name ( ) , ".yaml" ) {
return nil
}
b , err := os . ReadFile ( path )
if err != nil {
return err
}
var spec ServiceSpec
if err := yaml . Unmarshal ( b , & spec ) ; err != nil {
return err
}
specsByID [ spec . ServiceID ] = spec
return nil
} )
if walkErr != nil {
panic ( walkErr )
}
if len ( ordered ) == 0 {
specs := make ( [ ] ServiceSpec , 0 , len ( specsByID ) )
for _ , spec := range specsByID {
specs = append ( specs , spec )
}
sort . Slice ( specs , func ( i , j int ) bool { return specs [ i ] . ServiceID < specs [ j ] . ServiceID } )
return specs
}
specs := [ ] ServiceSpec { }
for _ , meta := range ordered {
spec , ok := specsByID [ meta . ID ]
if ! ok {
continue
}
specs = append ( specs , spec )
}
return specs
}
func writeResourceDocs ( docsDir string , spec ServiceSpec , version string , apiEndpoint string ) {
base := spec . Name
nav := fmt . Sprintf ( "[Manual](%s.md) | [Create params](%s_params_create.md) | [Modify params](%s_params_modify.md) | [Output params](%s_outputs.md) | [Operations](%s_ops.md) | [Example](%s_example.md)" , base , base , base , base , base , base )
writeFile ( filepath . Join ( docsDir , base + ".md" ) , buildManualPage ( spec , nav ) )
writeFile ( filepath . Join ( docsDir , base + "_example.md" ) , buildExamplePage ( spec , nav , version , apiEndpoint ) )
writeFile ( filepath . Join ( docsDir , base + "_params_create.md" ) , buildCreateParamsPage ( spec , nav ) )
writeFile ( filepath . Join ( docsDir , base + "_params_modify.md" ) , buildModifyParamsPage ( spec , nav ) )
writeFile ( filepath . Join ( docsDir , base + "_outputs.md" ) , buildOutputsPage ( spec , nav ) )
writeFile ( filepath . Join ( docsDir , base + "_ops.md" ) , buildOpsPage ( spec , nav , docsDir ) )
writeFile ( filepath . Join ( docsDir , base + "_params.md" ) , buildParamsLandingPage ( spec , nav ) )
}
func writeFile ( path , content string ) {
if err := os . WriteFile ( path , [ ] byte ( content ) , 0 o644 ) ; err != nil {
panic ( err )
}
}
func buildHeader ( spec ServiceSpec , nav string ) string {
name := spec . ServiceDisplayName
if name == "" {
name = spec . Name
}
return fmt . Sprintf ( "# Resource nubes_%s\n\nService ID: `%d`\n\nService Name: %s\n\n%s\n\n" , spec . Name , spec . ServiceID , name , nav )
}
func buildManualPage ( spec ServiceSpec , nav string ) string {
var b strings . Builder
b . WriteString ( buildHeader ( spec , nav ) )
b . WriteString ( "## MAN\n\n" )
man := strings . TrimSpace ( spec . ServiceMan )
if man == "" {
man = "Manual not available."
}
b . WriteString ( "<div class=\"man-content\">\n" )
b . WriteString ( man )
b . WriteString ( "\n</div>\n" )
return b . String ( )
}
func buildExamplePage ( spec ServiceSpec , nav , version string , apiEndpoint string ) string {
var b strings . Builder
b . WriteString ( buildHeader ( spec , nav ) )
b . WriteString ( fmt . Sprintf ( "## Copy-ready manifest (`%s_main.tf`)\n\n" , spec . Name ) )
b . WriteString ( "<div class=\"copy-ready-code\"><pre><code>" )
b . WriteString ( exampleBlock ( spec , version , apiEndpoint ) )
b . WriteString ( "</code></pre></div>\n\n" )
b . WriteString ( "## Outputs usage\n\n" )
b . WriteString ( "<div class=\"copy-ready-code\"><pre><code>" )
b . WriteString ( "# Выходные параметры можно использовать как\n" )
b . WriteString ( fmt . Sprintf ( "nubes_%s.baza.state_params[\"имя_ключа\"]\n" , spec . Name ) )
b . WriteString ( fmt . Sprintf ( "nubes_%s.baza.state_out[\"имя_ключа\"]\n" , spec . Name ) )
b . WriteString ( fmt . Sprintf ( "nubes_%s.baza.state_params_flat[\"имя_ключа\"]\n" , spec . Name ) )
b . WriteString ( fmt . Sprintf ( "nubes_%s.baza.state_out_flat[\"имя_ключа\"]\n" , spec . Name ) )
b . WriteString ( fmt . Sprintf ( "nubes_%s.baza.vault_secrets[\"имя_ключа\"]\n" , spec . Name ) )
b . WriteString ( fmt . Sprintf ( "nubes_%s.baza.vault_url\n" , spec . Name ) )
b . WriteString ( fmt . Sprintf ( "nubes_%s.baza.vault_user_path\n" , spec . Name ) )
b . WriteString ( fmt . Sprintf ( "nubes_%s.baza.vault_fields\n" , spec . Name ) )
b . WriteString ( "</code></pre></div>\n" )
return b . String ( )
}
func exampleBlock ( spec ServiceSpec , version string , apiEndpoint string ) string {
createParams := findParams ( spec . Operations , "create" )
requiredParams , defaultParams := splitParams ( createParams )
var b strings . Builder
b . WriteString ( "terraform {\n" )
b . WriteString ( " required_providers {\n" )
b . WriteString ( " nubes = {\n" )
b . WriteString ( " source = \"terra.k8c.ru/nubes/nubes\"\n" )
b . WriteString ( fmt . Sprintf ( " version = \"%s\"\n" , version ) )
b . WriteString ( " }\n" )
b . WriteString ( " }\n" )
b . WriteString ( "}\n\n" )
b . WriteString ( "# Токен доступа api_token к Nubes API — замените на реальный.\n" )
b . WriteString ( "provider \"nubes\" {\n" )
b . WriteString ( " api_token = \"token_***\"\n" )
b . WriteString ( fmt . Sprintf ( " api_endpoint = \"%s\"\n" , apiEndpoint ) )
b . WriteString ( "}\n" )
b . WriteString ( fmt . Sprintf ( "resource \"nubes_%s\" \"baza\" {\n" , spec . Name ) )
if len ( requiredParams ) > 0 {
b . WriteString ( " # TODO - обязательный параметр, задаваемый пользователем\n" )
}
for _ , p := range requiredParams {
b . WriteString ( formatParamLine ( p , " " ) )
}
if len ( defaultParams ) > 0 {
b . WriteString ( "\n # Параметры, имеющие значение по умолчанию, если не меняете - эти параметры не обязательно прописывать в манифесте\n" )
for _ , p := range defaultParams {
b . WriteString ( formatParamLine ( p , " " ) )
}
}
b . WriteString ( "}\n" )
return escapeCode ( b . String ( ) )
}
func buildCreateParamsPage ( spec ServiceSpec , nav string ) string {
var b strings . Builder
b . WriteString ( buildHeader ( spec , nav ) )
b . WriteString ( "## Create params\n\n" )
createParams := findParams ( spec . Operations , "create" )
requiredParams , defaultParams := splitParams ( createParams )
b . WriteString ( "**Обязательные параметры (вводимые пользователем)**\n\n" )
b . WriteString ( renderParamTable ( requiredParams , true , true ) )
b . WriteString ( "\n**Параметры, имеющие значение по умолчанию, если не меняете - эти параметры не обязательно прописывать в манифесте**\n" )
b . WriteString ( renderParamTable ( defaultParams , false , false ) )
lifecycle := spec . Lifecycle
if lifecycle . DeleteModeDefault != "" || lifecycle . ResumeIfExistsDefault {
b . WriteString ( "\n<div class=\"lifecycle-note\">\n" )
b . WriteString ( "<strong>Параметры жизненного цикла (Lifecycle defaults)</strong><br/>\n" )
b . WriteString ( fmt . Sprintf ( "По умолчанию: `delete_mode_default = %s`, `resume_if_exists_default = %v`.<br/>\n" , lifecycle . DeleteModeDefault , lifecycle . ResumeIfExistsDefault ) )
b . WriteString ( "Если для сервиса доступны операции `suspend` и `resume`, то `delete` выполняет приостановку (без удаления данных),<br/>\n" )
b . WriteString ( "а `apply` при необходимости возобновляет работу существующего ресурса.<br/>\n" )
b . WriteString ( "Менять эти значения стоит только в продвинутых сценариях — через параметры `delete_mode` и `resume_if_exists` в манифесте.\n" )
b . WriteString ( "</div>\n" )
}
return b . String ( )
}
func buildModifyParamsPage ( spec ServiceSpec , nav string ) string {
var b strings . Builder
b . WriteString ( buildHeader ( spec , nav ) )
b . WriteString ( "## Modify params\n\n" )
modifyParams := findParams ( spec . Operations , "modify" )
b . WriteString ( renderModifyTable ( modifyParams ) )
return b . String ( )
}
func buildOutputsPage ( spec ServiceSpec , nav string ) string {
var b strings . Builder
b . WriteString ( buildHeader ( spec , nav ) )
b . WriteString ( "## Output params\n\n" )
if len ( spec . Outputs . Params ) == 0 {
b . WriteString ( "None.\n" )
return b . String ( )
}
for _ , p := range spec . Outputs . Params {
b . WriteString ( fmt . Sprintf ( "- `%s` (%s)\n" , p . Code , p . Type ) )
}
return b . String ( )
}
func buildOpsPage ( spec ServiceSpec , nav , docsDir string ) string {
var b strings . Builder
b . WriteString ( buildHeader ( spec , nav ) )
b . WriteString ( "## Operations\n\n" )
for _ , op := range spec . Operations {
name := op . Name
if name == "" {
name = op . Action
}
descr := strings . TrimSpace ( stripHTML ( op . Man ) )
if descr != "" {
descr = ": " + descr
}
switch op . Action {
case "create" :
b . WriteString ( fmt . Sprintf ( "- `%s` — Создание кластера. Параметры: [%s params](%s_params_create.md)\n" , name , "Create" , spec . Name ) )
continue
case "modify" :
b . WriteString ( fmt . Sprintf ( "- `%s` — Изменение кластера. Параметры: [%s params](%s_params_modify.md)\n" , name , "Modify" , spec . Name ) )
continue
}
if op . Kind == "subresource" && op . Subresource != "" {
subFile := fmt . Sprintf ( "%s_%s.md" , spec . Name , slug ( op . Subresource ) )
if fileExists ( filepath . Join ( docsDir , subFile ) ) {
b . WriteString ( fmt . Sprintf ( "- `%s` — %s. См. [nubes_%s_%s](%s)\n" , name , strings . TrimPrefix ( descr , ": " ) , spec . Name , slug ( op . Subresource ) , subFile ) )
continue
}
}
if descr != "" {
b . WriteString ( fmt . Sprintf ( "- `%s` — %s\n" , name , strings . TrimPrefix ( descr , ": " ) ) )
} else {
b . WriteString ( fmt . Sprintf ( "- `%s`\n" , name ) )
}
}
return b . String ( )
}
func buildParamsLandingPage ( spec ServiceSpec , nav string ) string {
var b strings . Builder
b . WriteString ( buildHeader ( spec , nav ) )
b . WriteString ( "## Parameters\n\n" )
b . WriteString ( fmt . Sprintf ( "- [Create params](%s_params_create.md)\n" , spec . Name ) )
b . WriteString ( fmt . Sprintf ( "- [Modify params](%s_params_modify.md)\n" , spec . Name ) )
return b . String ( )
}
func findParams ( ops [ ] OperationSpec , action string ) [ ] ParamSpec {
for _ , op := range ops {
if op . Kind == "instance" && op . Action == action {
return op . Params
}
}
return nil
}
func splitParams ( params [ ] ParamSpec ) ( [ ] ParamSpec , [ ] ParamSpec ) {
required := [ ] ParamSpec { }
defaults := [ ] ParamSpec { }
for _ , p := range params {
if hasDefault ( p . Default ) {
defaults = append ( defaults , p )
continue
}
required = append ( required , p )
}
return required , defaults
}
func hasDefault ( value interface { } ) bool {
if value == nil {
return false
}
if s , ok := value . ( string ) ; ok {
return strings . TrimSpace ( s ) != ""
}
return true
}
func renderParamTable ( params [ ] ParamSpec , requiredTable , noDefault bool ) string {
if len ( params ) == 0 {
return "None.\n"
}
var b strings . Builder
tableClass := "resource-table resource-table-compact"
if requiredTable {
tableClass += " resource-table-required"
}
b . WriteString ( fmt . Sprintf ( "<table class=\"%s\">\n" , tableClass ) )
if noDefault {
b . WriteString ( "<thead><tr><th>ID</th><th>Code</th><th>Type</th><th>Description</th><th>Constraints</th></tr></thead>\n<tbody>\n" )
} else {
b . WriteString ( "<thead><tr><th>ID</th><th>Code</th><th>Type</th><th>Default</th><th>Description</th><th>Constraints</th></tr></thead>\n<tbody>\n" )
}
for _ , p := range params {
b . WriteString ( "<tr>" )
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , escapeText ( formatID ( p . ID ) ) ) )
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , formatCode ( p . Code ) ) )
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , formatTypeCell ( p ) ) )
if ! noDefault {
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , defaultCell ( p . Default ) ) )
}
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , escapeText ( pickText ( p ) ) ) )
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , escapeText ( collectConstraints ( p ) ) ) )
b . WriteString ( "</tr>\n" )
}
b . WriteString ( "</tbody></table>\n" )
return b . String ( )
}
func renderModifyTable ( params [ ] ParamSpec ) string {
if len ( params ) == 0 {
return "None.\n"
}
var b strings . Builder
b . WriteString ( "<table class=\"resource-table resource-table-compact\">\n" )
b . WriteString ( "<thead><tr><th>ID</th><th>Code</th><th>Type</th><th>Description</th><th>Constraints</th></tr></thead>\n<tbody>\n" )
for _ , p := range params {
b . WriteString ( "<tr>" )
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , escapeText ( formatID ( p . ID ) ) ) )
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , formatCode ( p . Code ) ) )
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , formatTypeCell ( p ) ) )
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , escapeText ( pickText ( p ) ) ) )
b . WriteString ( fmt . Sprintf ( "<td>%s</td>" , escapeText ( collectConstraints ( p ) ) ) )
b . WriteString ( "</tr>\n" )
}
b . WriteString ( "</tbody></table>\n" )
return b . String ( )
}
func formatParamLine ( p ParamSpec , indent string ) string {
value := sampleValue ( p )
comment := strings . TrimSpace ( stripHTML ( p . Man ) )
if comment == "" {
comment = strings . TrimSpace ( stripHTML ( p . Descr ) )
}
if comment != "" {
return fmt . Sprintf ( "%s%s = %s # %s\n" , indent , p . Code , value , comment )
}
return fmt . Sprintf ( "%s%s = %s\n" , indent , p . Code , value )
}
func sampleValue ( p ParamSpec ) string {
if hasDefault ( p . Default ) {
return formatLiteral ( p . Default )
}
if len ( p . ValueList ) > 0 {
return formatLiteral ( p . ValueList [ 0 ] )
}
dtype := strings . ToLower ( firstType ( p ) )
switch {
case strings . Contains ( dtype , "bool" ) :
return "false"
case strings . Contains ( dtype , "int" ) || strings . Contains ( dtype , "number" ) :
return "1"
case strings . Contains ( dtype , "json" ) :
return "{}"
}
return "\"TODO\""
}
func formatLiteral ( value interface { } ) string {
switch v := value . ( type ) {
case bool :
if v {
return "true"
}
return "false"
case int :
return strconv . Itoa ( v )
case int64 :
return strconv . FormatInt ( v , 10 )
case float64 :
return strconv . FormatFloat ( v , 'f' , - 1 , 64 )
case string :
if strings . HasPrefix ( v , "{" ) || strings . HasPrefix ( v , "[" ) {
return v
}
if strings . HasPrefix ( v , "\"" ) && strings . HasSuffix ( v , "\"" ) {
return v
}
if looksLikeNumber ( v ) || v == "true" || v == "false" {
return v
}
return fmt . Sprintf ( "\"%s\"" , v )
default :
return fmt . Sprintf ( "\"%v\"" , v )
}
}
func looksLikeNumber ( value string ) bool {
if value == "" {
return false
}
if value [ 0 ] == '-' {
value = value [ 1 : ]
}
for _ , ch := range value {
if ch == '.' {
continue
}
if ch < '0' || ch > '9' {
return false
}
}
return true
}
func formatID ( id int ) string {
if id == 0 {
return ""
}
return strconv . Itoa ( id )
}
func formatTypeCell ( p ParamSpec ) string {
dtype := firstType ( p )
if dtype == "" {
return ""
}
return "<code>" + escapeText ( dtype ) + "</code>"
}
func firstType ( p ParamSpec ) string {
if p . DataType != "" {
return normalizeType ( p . DataType )
}
return normalizeType ( p . Type )
}
func normalizeType ( dtype string ) string {
cleaned := strings . ReplaceAll ( dtype , ">" , ">" )
if idx := strings . Index ( cleaned , ">" ) ; idx >= 0 {
cleaned = cleaned [ : idx ]
}
return strings . TrimSpace ( cleaned )
}
func defaultCell ( value interface { } ) string {
if ! hasDefault ( value ) {
return ""
}
return "<code>" + escapeText ( formatLiteral ( value ) ) + "</code>"
}
func pickText ( p ParamSpec ) string {
if p . Descr == "" && p . Man == "" {
return ""
}
if p . Man == "" {
return p . Descr
}
if p . Descr == "" {
return p . Man
}
if len ( p . Man ) > len ( p . Descr ) {
return p . Man
}
return p . Descr
}
func collectConstraints ( p ParamSpec ) string {
parts := [ ] string { }
if p . MinValue != nil {
parts = append ( parts , fmt . Sprintf ( "minvalue=%d" , * p . MinValue ) )
}
if p . MaxValue != nil {
parts = append ( parts , fmt . Sprintf ( "maxvalue=%d" , * p . MaxValue ) )
}
if p . Regex != "" {
parts = append ( parts , fmt . Sprintf ( "regex=%s" , p . Regex ) )
}
if len ( p . ValueList ) > 0 {
parts = append ( parts , fmt . Sprintf ( "value_list=%s" , strings . Join ( p . ValueList , ", " ) ) )
}
if p . Func != "" {
parts = append ( parts , fmt . Sprintf ( "func=%s" , p . Func ) )
}
if p . RefSvcID != nil {
parts = append ( parts , fmt . Sprintf ( "ref_svc_id=%d" , * p . RefSvcID ) )
}
if p . Unique != "" {
parts = append ( parts , fmt . Sprintf ( "unique_scope=%s" , p . Unique ) )
}
if p . MaxLength != nil {
parts = append ( parts , fmt . Sprintf ( "maxlength=%d" , * p . MaxLength ) )
}
if p . MinLength != nil {
parts = append ( parts , fmt . Sprintf ( "minlength=%d" , * p . MinLength ) )
}
return strings . Join ( parts , "; " )
}
func escapeText ( value string ) string {
if value == "" {
return ""
}
text := html . EscapeString ( value )
text = strings . ReplaceAll ( text , "<br/>" , "<br/>" )
text = strings . ReplaceAll ( text , "<br />" , "<br/>" )
text = strings . ReplaceAll ( text , "<br>" , "<br/>" )
return text
}
func escapeCode ( value string ) string {
buf := bytes . NewBuffer ( nil )
for _ , line := range strings . Split ( value , "\n" ) {
buf . WriteString ( html . EscapeString ( line ) )
buf . WriteString ( "\n" )
}
return buf . String ( )
}
func formatCode ( code string ) string {
if code == "" {
return ""
}
if len ( code ) <= 40 {
return "<strong><code class=\"code-no-wrap\">" + code + "</code></strong>"
}
out := [ ] string { }
prev := rune ( 0 )
for _ , ch := range code {
if ch == '_' {
out = append ( out , "_<wbr>" )
prev = ch
continue
}
if prev != 0 && ( unicodeIsLower ( prev ) || unicodeIsDigit ( prev ) ) && unicodeIsUpper ( ch ) {
out = append ( out , "<wbr>" )
}
out = append ( out , string ( ch ) )
prev = ch
}
return "<strong><code class=\"code-wrap\">" + strings . Join ( out , "" ) + "</code></strong>"
}
func unicodeIsLower ( r rune ) bool { return r >= 'a' && r <= 'z' }
func unicodeIsUpper ( r rune ) bool { return r >= 'A' && r <= 'Z' }
func unicodeIsDigit ( r rune ) bool { return r >= '0' && r <= '9' }
func stripHTML ( value string ) string {
if value == "" {
return ""
}
re := regexp . MustCompile ( "<[^>]+>" )
cleaned := re . ReplaceAllString ( value , " " )
cleaned = strings . Join ( strings . Fields ( cleaned ) , " " )
return html . UnescapeString ( cleaned )
}
func slug ( value string ) string {
out := [ ] rune { }
for _ , ch := range value {
if ( ch >= 'a' && ch <= 'z' ) || ( ch >= '0' && ch <= '9' ) || ch == '_' || ch == '-' {
out = append ( out , ch )
} else if ch >= 'A' && ch <= 'Z' {
out = append ( out , ch + 'a' - 'A' )
} else {
out = append ( out , '_' )
}
}
return strings . Trim ( string ( out ) , "_" )
}
func fileExists ( path string ) bool {
_ , err := os . Stat ( path )
return err == nil
}