Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions internal/graph2md/graph2md.go
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,10 @@ type graphNode struct {
Label string `json:"label"`
Type string `json:"type"`
Slug string `json:"slug"`
LC int `json:"lc,omitempty"` // line count
Lang string `json:"lang,omitempty"` // language
CC int `json:"cc,omitempty"` // call count (calls out)
CBC int `json:"cbc,omitempty"` // called by count
}

type graphEdge struct {
Expand Down Expand Up @@ -1499,11 +1503,26 @@ func (c *renderContext) writeGraphData(sb *strings.Builder) {
if len(n.Labels) > 0 {
nodeType = n.Labels[0]
}
// Enrichment data
lineCount := 0
startLine := getNum(n.Properties, "startLine")
endLine := getNum(n.Properties, "endLine")
if startLine > 0 && endLine > 0 {
lineCount = endLine - startLine + 1
}
lang := getStr(n.Properties, "language")
callCount := len(c.calls[nodeID])
calledByCount := len(c.calledBy[nodeID])

nodes = append(nodes, graphNode{
ID: nodeID,
Label: label,
Type: nodeType,
Slug: c.slugLookup[nodeID],
LC: lineCount,
Lang: lang,
CC: callCount,
CBC: calledByCount,
})
}

Expand Down
216 changes: 199 additions & 17 deletions internal/pssg/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,102 @@ func (b *Builder) renderEntityPage(
title := e.GetString("title")
description := e.GetString("description")

// Entity profile chart data (compact format for JS)
profileData := map[string]interface{}{}
if lc := e.GetInt("line_count"); lc > 0 {
profileData["lc"] = lc
}
if co := e.GetInt("call_count"); co > 0 {
profileData["co"] = co
}
if cb := e.GetInt("called_by_count"); cb > 0 {
profileData["cb"] = cb
}
if ic := e.GetInt("import_count"); ic > 0 {
profileData["ic"] = ic
}
if ib := e.GetInt("imported_by_count"); ib > 0 {
profileData["ib"] = ib
}
if fn := e.GetInt("function_count"); fn > 0 {
profileData["fn"] = fn
}
if cl := e.GetInt("class_count"); cl > 0 {
profileData["cl"] = cl
}
if tc := e.GetInt("type_count"); tc > 0 {
profileData["tc"] = tc
}
if fc := e.GetInt("file_count"); fc > 0 {
profileData["fc"] = fc
}
if sl := e.GetInt("start_line"); sl > 0 {
profileData["sl"] = sl
}
if el := e.GetInt("end_line"); el > 0 {
profileData["el"] = el
}
// Edge type breakdown
edgeTypes := map[string]int{}
if v := e.GetInt("import_count"); v > 0 {
edgeTypes["imports"] = v
}
if v := e.GetInt("imported_by_count"); v > 0 {
edgeTypes["imports"] += v
}
if v := e.GetInt("call_count"); v > 0 {
edgeTypes["calls"] = v
}
if v := e.GetInt("called_by_count"); v > 0 {
edgeTypes["calls"] += v
}
if v := e.GetInt("function_count"); v > 0 {
edgeTypes["defines"] += v
}
if v := e.GetInt("class_count"); v > 0 {
edgeTypes["defines"] += v
}
if v := e.GetInt("type_count"); v > 0 {
edgeTypes["defines"] += v
}
if len(edgeTypes) > 0 {
profileData["et"] = edgeTypes
}
var entityChartJSON []byte
if len(profileData) > 0 {
entityChartJSON, _ = json.Marshal(profileData)
}

// Source code (read from workspace if available)
var sourceCode, sourceLang string
if filePath := e.GetString("file_path"); filePath != "" {
if sl := e.GetInt("start_line"); sl > 0 {
if el := e.GetInt("end_line"); el > 0 {
sourceDir := b.cfg.Paths.SourceDir
if sourceDir != "" {
fullPath := filepath.Join(sourceDir, filePath)
if data, err := os.ReadFile(fullPath); err == nil {
lines := strings.Split(string(data), "\n")
if sl <= len(lines) && el <= len(lines) {
sourceCode = strings.Join(lines[sl-1:el], "\n")
}
}
}
}
}
sourceLang = e.GetString("language")
if sourceLang == "" {
ext := filepath.Ext(filePath)
langMap := map[string]string{
".js": "javascript", ".ts": "typescript", ".tsx": "typescript",
".py": "python", ".go": "go", ".rs": "rust", ".java": "java",
".rb": "ruby", ".php": "php", ".c": "c", ".cpp": "cpp",
".cs": "csharp", ".swift": "swift", ".kt": "kotlin",
}
sourceLang = langMap[ext]
}
}

ctx := render.EntityPageContext{
Site: b.cfg.Site,
Entity: e,
Expand All @@ -410,6 +506,9 @@ func (b *Builder) renderEntityPage(
AllTaxonomies: taxonomies,
ValidSlugs: validSlugs,
Contributors: contributors,
ChartData: template.JS(entityChartJSON),
SourceCode: sourceCode,
SourceLang: sourceLang,
CTA: b.cfg.Extra.CTA,
OG: render.OGMeta{
Title: title + " \u2014 " + b.cfg.Site.Name,
Expand Down Expand Up @@ -551,7 +650,7 @@ func (b *Builder) renderTaxonomyPages(
Type: "article",
SiteName: b.cfg.Site.Name,
},
ChartData: template.HTML(hubChartJSON),
ChartData: template.JS(hubChartJSON),
CTA: b.cfg.Extra.CTA,
}

Expand Down Expand Up @@ -650,7 +749,7 @@ func (b *Builder) renderTaxonomyPages(
Type: "article",
SiteName: b.cfg.Site.Name,
},
ChartData: template.HTML(taxChartJSON),
ChartData: template.JS(taxChartJSON),
CTA: b.cfg.Extra.CTA,
}

Expand Down Expand Up @@ -724,7 +823,7 @@ func (b *Builder) renderTaxonomyPages(
Type: "article",
SiteName: b.cfg.Site.Name,
},
ChartData: template.HTML(letterChartJSON),
ChartData: template.JS(letterChartJSON),
CTA: b.cfg.Extra.CTA,
}

Expand Down Expand Up @@ -845,9 +944,9 @@ func (b *Builder) renderAllEntitiesPages(
jsonLD := schema.MarshalSchemas(collectionSchema, breadcrumbSchema)

// Only include chart data on page 1
var pageChartData template.HTML
var pageChartData template.JS
if page == 1 {
pageChartData = template.HTML(chartJSON)
pageChartData = template.JS(chartJSON)
}

ctx := render.AllEntitiesPageContext{
Expand Down Expand Up @@ -911,30 +1010,112 @@ func (b *Builder) renderHomepage(
}
imageURL := shareImageURL(b.cfg.Site.BaseURL, "homepage.svg")

// Chart data: treemap of taxonomies -> entries
type chartEntry struct {
// Chart data: treemap of taxonomies
type chartTax struct {
Name string `json:"name"`
Count int `json:"count"`
}
type chartTax struct {
Label string `json:"label"`
TopEntries []chartEntry `json:"topEntries"`
Slug string `json:"slug"`
}
type homepageChart struct {
Taxonomies []chartTax `json:"taxonomies"`
TotalEntities int `json:"totalEntities"`
}
var chartTaxonomies []chartTax
for _, tax := range taxonomies {
top := taxonomy.TopEntries(tax.Entries, 10)
var entries []chartEntry
for _, e := range top {
entries = append(entries, chartEntry{Name: e.Name, Count: len(e.Entities)})
totalCount := 0
for _, entry := range tax.Entries {
totalCount += len(entry.Entities)
}
chartTaxonomies = append(chartTaxonomies, chartTax{Label: tax.Label, TopEntries: entries})
chartTaxonomies = append(chartTaxonomies, chartTax{
Name: tax.Label,
Count: totalCount,
Slug: tax.Name,
})
}
chartJSON, _ := json.Marshal(homepageChart{Taxonomies: chartTaxonomies, TotalEntities: len(entities)})

// Architecture overview: domain/subdomain force graph
type archNode struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Count int `json:"count"`
Slug string `json:"slug,omitempty"`
}
type archLink struct {
Source string `json:"source"`
Target string `json:"target"`
}
type archOverview struct {
Nodes []archNode `json:"nodes"`
Links []archLink `json:"links"`
}

var archNodes []archNode
var archLinks []archLink

// Root node is the repo/site
rootID := "__root__"
archNodes = append(archNodes, archNode{ID: rootID, Name: b.cfg.Site.Name, Type: "root", Count: len(entities)})

// Find subdomain -> domain parent relationships
subdomainParent := make(map[string]string) // subdomain name -> domain name
for _, tax := range taxonomies {
if tax.Name == "subdomain" {
for _, entry := range tax.Entries {
parentDomain := ""
if len(entry.Entities) > 0 {
parentDomain = entry.Entities[0].GetString("domain")
}
subdomainParent[entry.Name] = parentDomain
}
}
}

// Add domain nodes
for _, tax := range taxonomies {
if tax.Name == "domain" {
for _, entry := range tax.Entries {
nodeID := "domain:" + entry.Slug
archNodes = append(archNodes, archNode{
ID: nodeID,
Name: entry.Name,
Type: "domain",
Count: len(entry.Entities),
Slug: "domain/" + entry.Slug,
})
archLinks = append(archLinks, archLink{Source: rootID, Target: nodeID})
}
}
}
// Add subdomain nodes
for _, tax := range taxonomies {
if tax.Name == "subdomain" {
for _, entry := range tax.Entries {
nodeID := "subdomain:" + entry.Slug
archNodes = append(archNodes, archNode{
ID: nodeID,
Name: entry.Name,
Type: "subdomain",
Count: len(entry.Entities),
Slug: "subdomain/" + entry.Slug,
})
parentDomain := subdomainParent[entry.Name]
if parentDomain != "" {
parentSlug := entity.ToSlug(parentDomain)
archLinks = append(archLinks, archLink{Source: "domain:" + parentSlug, Target: nodeID})
} else {
archLinks = append(archLinks, archLink{Source: rootID, Target: nodeID})
}
}
}
}

var archJSON []byte
if len(archNodes) > 1 {
archJSON, _ = json.Marshal(archOverview{Nodes: archNodes, Links: archLinks})
}

// JSON-LD
websiteSchema := schemaGen.GenerateWebSiteSchema(imageURL)

Expand Down Expand Up @@ -970,7 +1151,8 @@ func (b *Builder) renderHomepage(
Type: "website",
SiteName: b.cfg.Site.Name,
},
ChartData: template.HTML(chartJSON),
ChartData: template.JS(chartJSON),
ArchData: template.JS(archJSON),
CTA: b.cfg.Extra.CTA,
}

Expand Down
1 change: 1 addition & 0 deletions internal/pssg/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type PathsConfig struct {
Output string `yaml:"output"`
Cache string `yaml:"cache"`
Static string `yaml:"static"`
SourceDir string `yaml:"source_dir"`
}

type DataConfig struct {
Expand Down
21 changes: 8 additions & 13 deletions internal/pssg/render/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"html/template"
"math"
"net/url"
"reflect"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -257,19 +258,13 @@ func sliceHelper(items interface{}, start, end int) interface{} {
}

func length(v interface{}) int {
switch val := v.(type) {
case []string:
return len(val)
case []*entity.Entity:
return len(val)
case []interface{}:
return len(val)
case string:
return len(val)
case map[string]interface{}:
return len(val)
case []map[string]interface{}:
return len(val)
if v == nil {
return 0
}
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Slice, reflect.Map, reflect.Array, reflect.String:
return rv.Len()
}
return 0
}
Expand Down
Loading