Add Markdown index HTML output mode
This commit is contained in:
@@ -372,6 +372,59 @@ func TestRunPublishesHTMLOnly(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPublishesHTMLIndexWithExplicitInput(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{
|
||||
ExtraFiles: []testFile{{Path: "notes.md", Data: "# Notes\nHidden.\n"}},
|
||||
})
|
||||
|
||||
err := Run(context.Background(), RunOptions{ConfigPath: writeLocalConfigWithMarkdownTransform(t, sourceRoot, destinationRoot, false, true, config.TransformModeIndex, "report.md")})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
assertFileContains(t, filepath.Join(destinationRoot, "index.html"), "<h1>Report</h1>")
|
||||
if _, err := os.Stat(filepath.Join(destinationRoot, "report.html")); !os.IsNotExist(err) {
|
||||
t.Fatalf("report.html stat error = %v, want not exist", err)
|
||||
}
|
||||
destinationState := readStateFile(t, filepath.Join(destinationRoot, storage.StateFileName))
|
||||
if got, want := len(destinationState.Outputs), 1; got != want {
|
||||
t.Fatalf("state output count = %d, want %d", got, want)
|
||||
}
|
||||
output := destinationState.Outputs[0]
|
||||
if output.Kind != state.OutputKindGenerated || output.Transform != "markdown_to_html" || output.Path != "index.html" || output.SourcePath != "report.md" {
|
||||
t.Fatalf("generated output metadata = %#v", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPublishesHTMLIndexWithSingleMarkdownFallback(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
|
||||
|
||||
err := Run(context.Background(), RunOptions{ConfigPath: writeLocalConfigWithMarkdownTransform(t, sourceRoot, destinationRoot, false, true, config.TransformModeIndex, "")})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
assertFileContains(t, filepath.Join(destinationRoot, "index.html"), "<h1>Report</h1>")
|
||||
}
|
||||
|
||||
func TestRunFailsIndexModeWithAmbiguousMarkdownInput(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{
|
||||
ExtraFiles: []testFile{{Path: "notes.md", Data: "# Notes\n"}},
|
||||
})
|
||||
|
||||
err := Run(context.Background(), RunOptions{ConfigPath: writeLocalConfigWithMarkdownTransform(t, sourceRoot, destinationRoot, false, true, config.TransformModeIndex, "")})
|
||||
if err == nil || !strings.Contains(err.Error(), "multiple markdown source files") {
|
||||
t.Fatalf("Run() error = %v, want ambiguous input error", err)
|
||||
}
|
||||
if entries, err := os.ReadDir(destinationRoot); err != nil || len(entries) != 0 {
|
||||
t.Fatalf("destination entries = %v err=%v, want empty", entries, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPublishesSourceAndHTML(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
@@ -427,6 +480,22 @@ func TestRunFailsOnOutputPathCollision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFailsOnIndexOutputPathCollision(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{
|
||||
ExtraFiles: []testFile{{Path: "index.html", Data: "<p>source index</p>\n"}},
|
||||
})
|
||||
|
||||
err := Run(context.Background(), RunOptions{ConfigPath: writeLocalConfigWithMarkdownTransform(t, sourceRoot, destinationRoot, true, true, config.TransformModeIndex, "report.md")})
|
||||
if err == nil || !strings.Contains(err.Error(), "destination output path collision") {
|
||||
t.Fatalf("Run() error = %v, want collision", err)
|
||||
}
|
||||
if entries, err := os.ReadDir(destinationRoot); err != nil || len(entries) != 0 {
|
||||
t.Fatalf("destination entries = %v err=%v, want empty", entries, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDryRunReportsGeneratedOutputs(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
@@ -446,6 +515,73 @@ func TestRunDryRunReportsGeneratedOutputs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDryRunReportsIndexOutputWithoutWriting(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
|
||||
|
||||
var stdout bytes.Buffer
|
||||
err := Run(context.Background(), RunOptions{
|
||||
ConfigPath: writeLocalConfigWithMarkdownTransform(t, sourceRoot, destinationRoot, false, true, config.TransformModeIndex, ""),
|
||||
DryRun: true,
|
||||
Stdout: &stdout,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "outputs=index.html") {
|
||||
t.Fatalf("stdout = %q, want index output path", stdout.String())
|
||||
}
|
||||
if entries, err := os.ReadDir(destinationRoot); err != nil || len(entries) != 0 {
|
||||
t.Fatalf("destination entries = %v err=%v, want empty", entries, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSourceOnlyDoesNotWriteIndexOutput(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
|
||||
|
||||
err := Run(context.Background(), RunOptions{ConfigPath: writeLocalConfigWithMarkdownTransform(t, sourceRoot, destinationRoot, true, false, config.TransformModeIndex, "")})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
assertFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nSunny.\n")
|
||||
if _, err := os.Stat(filepath.Join(destinationRoot, "index.html")); !os.IsNotExist(err) {
|
||||
t.Fatalf("index.html stat error = %v, want not exist", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunReplacesHTMLIndexOutput(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{
|
||||
Created: testutil.DefaultCreated,
|
||||
Files: []testFile{
|
||||
{Path: "report.md", Data: "# Report\nOld.\n"},
|
||||
{Path: "summary.txt", Data: "Summary\n"},
|
||||
},
|
||||
})
|
||||
configPath := writeLocalConfigWithMarkdownTransform(t, sourceRoot, destinationRoot, false, true, config.TransformModeIndex, "")
|
||||
if err := Run(context.Background(), RunOptions{ConfigPath: configPath}); err != nil {
|
||||
t.Fatalf("first Run() error = %v", err)
|
||||
}
|
||||
assertFileContains(t, filepath.Join(destinationRoot, "index.html"), "<p>Old.</p>")
|
||||
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{
|
||||
Created: testutil.DefaultCreated.Add(time.Hour),
|
||||
Files: []testFile{
|
||||
{Path: "report.md", Data: "# Report\nNew.\n"},
|
||||
{Path: "summary.txt", Data: "Summary\n"},
|
||||
},
|
||||
})
|
||||
|
||||
if err := Run(context.Background(), RunOptions{ConfigPath: configPath}); err != nil {
|
||||
t.Fatalf("second Run() error = %v", err)
|
||||
}
|
||||
assertFileContains(t, filepath.Join(destinationRoot, "index.html"), "<p>New.</p>")
|
||||
}
|
||||
|
||||
func TestRunSkipsWhenDestinationStateMatches(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
@@ -727,6 +863,7 @@ func TestRunDryRunDoesNotWrite(t *testing.T) {
|
||||
type testBundleOptions struct {
|
||||
ID string
|
||||
Created time.Time
|
||||
Files []testFile
|
||||
ExtraFiles []testFile
|
||||
}
|
||||
|
||||
@@ -737,6 +874,13 @@ type testFile struct {
|
||||
|
||||
func writeSourceBundle(t *testing.T, root, relative string, opts testBundleOptions) bundle.Manifest {
|
||||
t.Helper()
|
||||
var files []testutil.SourceFile
|
||||
if opts.Files != nil {
|
||||
files = make([]testutil.SourceFile, 0, len(opts.Files))
|
||||
for _, file := range opts.Files {
|
||||
files = append(files, testutil.SourceFile{Path: file.Path, Data: file.Data})
|
||||
}
|
||||
}
|
||||
extraFiles := make([]testutil.SourceFile, 0, len(opts.ExtraFiles))
|
||||
for _, file := range opts.ExtraFiles {
|
||||
extraFiles = append(extraFiles, testutil.SourceFile{Path: file.Path, Data: file.Data})
|
||||
@@ -744,6 +888,7 @@ func writeSourceBundle(t *testing.T, root, relative string, opts testBundleOptio
|
||||
return testutil.WriteSourceBundle(t, root, relative, testutil.BundleOptions{
|
||||
ID: opts.ID,
|
||||
Created: opts.Created,
|
||||
Files: files,
|
||||
ExtraFiles: extraFiles,
|
||||
})
|
||||
}
|
||||
@@ -779,6 +924,34 @@ pipelines:
|
||||
`)
|
||||
}
|
||||
|
||||
func writeLocalConfigWithMarkdownTransform(t *testing.T, sourceRoot, destinationRoot string, publishSource, publishHTML bool, mode, input string) string {
|
||||
t.Helper()
|
||||
enabled := publishHTML
|
||||
inputConfig := ""
|
||||
if input != "" {
|
||||
inputConfig = `
|
||||
input: ` + input
|
||||
}
|
||||
return writeConfigFile(t, `
|
||||
pipelines:
|
||||
- id: reports
|
||||
source:
|
||||
backend: local
|
||||
path: `+sourceRoot+`
|
||||
destinations:
|
||||
- id: archive
|
||||
backend: local
|
||||
path: `+destinationRoot+`
|
||||
publish:
|
||||
source: `+fmt.Sprintf("%t", publishSource)+`
|
||||
html: `+fmt.Sprintf("%t", publishHTML)+`
|
||||
transform:
|
||||
markdown_to_html:
|
||||
enabled: `+fmt.Sprintf("%t", enabled)+`
|
||||
mode: `+mode+inputConfig+`
|
||||
`)
|
||||
}
|
||||
|
||||
func writeFanoutConfig(t *testing.T, sourceRoot, firstDestination, secondDestination string) string {
|
||||
t.Helper()
|
||||
return testutil.WriteFanoutLocalConfig(t, sourceRoot, firstDestination, secondDestination)
|
||||
|
||||
@@ -77,6 +77,7 @@ type Transform struct {
|
||||
type MarkdownToHTML struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Mode string `yaml:"mode"`
|
||||
Input string `yaml:"input"`
|
||||
}
|
||||
|
||||
type TransferPolicy struct {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package config
|
||||
|
||||
import "gitea.maximumdirect.net/eric/distributor/internal/transform"
|
||||
|
||||
const DefaultConfigPath = "/usr/local/etc/distributor/config.yml"
|
||||
|
||||
const (
|
||||
@@ -19,7 +21,8 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
TransformModeSidecar = "sidecar"
|
||||
TransformModeSidecar = transform.MarkdownModeSidecar
|
||||
TransformModeIndex = transform.MarkdownModeIndex
|
||||
)
|
||||
|
||||
const DefaultS3Region = "us-east-1"
|
||||
@@ -37,6 +40,9 @@ func ApplyDefaults(cfg *Config) {
|
||||
if destination.Publish == nil {
|
||||
destination.Publish = &PublishPolicy{Source: true}
|
||||
}
|
||||
if destination.Transform.MarkdownToHTML != nil && destination.Transform.MarkdownToHTML.Mode == "" {
|
||||
destination.Transform.MarkdownToHTML.Mode = TransformModeSidecar
|
||||
}
|
||||
if destination.Transfer.OnDestinationSame == "" {
|
||||
destination.Transfer.OnDestinationSame = TransferActionSkip
|
||||
}
|
||||
|
||||
@@ -94,6 +94,58 @@ pipelines:
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileValidMarkdownIndexConfig(t *testing.T) {
|
||||
cfg := loadConfig(t, `
|
||||
pipelines:
|
||||
- id: static-site
|
||||
source:
|
||||
backend: local
|
||||
path: /var/spool/reports
|
||||
destinations:
|
||||
- id: web
|
||||
backend: local
|
||||
path: /srv/www/reports
|
||||
publish:
|
||||
source: false
|
||||
html: true
|
||||
transform:
|
||||
markdown_to_html:
|
||||
enabled: true
|
||||
mode: index
|
||||
input: report.md
|
||||
`)
|
||||
|
||||
markdown := cfg.Pipelines[0].Destinations[0].Transform.MarkdownToHTML
|
||||
if markdown == nil || markdown.Mode != TransformModeIndex || markdown.Input != "report.md" {
|
||||
t.Fatalf("markdown config = %#v, want index input", markdown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileDefaultsMarkdownModeToSidecar(t *testing.T) {
|
||||
cfg := loadConfig(t, `
|
||||
pipelines:
|
||||
- id: static-site
|
||||
source:
|
||||
backend: local
|
||||
path: /var/spool/reports
|
||||
destinations:
|
||||
- id: web
|
||||
backend: local
|
||||
path: /srv/www/reports
|
||||
publish:
|
||||
source: false
|
||||
html: true
|
||||
transform:
|
||||
markdown_to_html:
|
||||
enabled: true
|
||||
`)
|
||||
|
||||
markdown := cfg.Pipelines[0].Destinations[0].Transform.MarkdownToHTML
|
||||
if markdown == nil || markdown.Mode != TransformModeSidecar {
|
||||
t.Fatalf("markdown mode = %#v, want sidecar default", markdown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadFileValidBackendConfigs(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"local": `
|
||||
@@ -464,6 +516,7 @@ func TestExampleConfigsLoad(t *testing.T) {
|
||||
"../../examples/local-to-local.yml",
|
||||
"../../examples/local-publish.yml",
|
||||
"../../examples/local-html.yml",
|
||||
"../../examples/local-index.yml",
|
||||
"../../examples/fan-out.yml",
|
||||
"../../examples/ssh-destination.yml",
|
||||
"../../examples/s3-destination.yml",
|
||||
|
||||
@@ -149,18 +149,25 @@ func ValidatePublishTransformPolicy(publish PublishPolicy, transform Transform)
|
||||
if transform.MarkdownToHTML == nil {
|
||||
return nil
|
||||
}
|
||||
mode := transform.MarkdownToHTML.Mode
|
||||
if mode == "" {
|
||||
mode = TransformModeSidecar
|
||||
}
|
||||
if mode != TransformModeSidecar && mode != TransformModeIndex {
|
||||
return fmt.Errorf("transform.markdown_to_html.mode must be %s or %s", TransformModeSidecar, TransformModeIndex)
|
||||
}
|
||||
if transform.MarkdownToHTML.Input != "" && !transform.MarkdownToHTML.Enabled {
|
||||
return fmt.Errorf("transform.markdown_to_html.input requires transform.markdown_to_html.enabled to be true")
|
||||
}
|
||||
if transform.MarkdownToHTML.Input != "" && mode != TransformModeIndex {
|
||||
return fmt.Errorf("transform.markdown_to_html.input is only valid when mode is %s", TransformModeIndex)
|
||||
}
|
||||
if transform.MarkdownToHTML.Enabled && !publish.HTML {
|
||||
return fmt.Errorf("transform.markdown_to_html.enabled requires publish.html to be true")
|
||||
}
|
||||
if publish.HTML && !transform.MarkdownToHTML.Enabled {
|
||||
return fmt.Errorf("transform.markdown_to_html.enabled must be true when publish.html is true")
|
||||
}
|
||||
if publish.HTML && transform.MarkdownToHTML.Mode != TransformModeSidecar {
|
||||
return fmt.Errorf("transform.markdown_to_html.mode must be %s", TransformModeSidecar)
|
||||
}
|
||||
if transform.MarkdownToHTML.Enabled && transform.MarkdownToHTML.Mode != TransformModeSidecar {
|
||||
return fmt.Errorf("transform.markdown_to_html.mode must be %s", TransformModeSidecar)
|
||||
}
|
||||
if !transform.MarkdownToHTML.Enabled && transform.MarkdownToHTML.Mode != "" && transform.MarkdownToHTML.Mode != TransformModeSidecar {
|
||||
return fmt.Errorf("transform.markdown_to_html.mode must be %s", TransformModeSidecar)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,30 @@ func publishTransformPolicyCases() []publishTransformPolicyCase {
|
||||
Mode: TransformModeSidecar,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "html only default mode allowed",
|
||||
publish: PublishPolicy{HTML: true},
|
||||
transform: Transform{MarkdownToHTML: &MarkdownToHTML{
|
||||
Enabled: true,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "html only index allowed",
|
||||
publish: PublishPolicy{HTML: true},
|
||||
transform: Transform{MarkdownToHTML: &MarkdownToHTML{
|
||||
Enabled: true,
|
||||
Mode: TransformModeIndex,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "html only index input allowed",
|
||||
publish: PublishPolicy{HTML: true},
|
||||
transform: Transform{MarkdownToHTML: &MarkdownToHTML{
|
||||
Enabled: true,
|
||||
Mode: TransformModeIndex,
|
||||
Input: "report.md",
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "source and html sidecar allowed",
|
||||
publish: PublishPolicy{Source: true, HTML: true},
|
||||
@@ -127,6 +151,15 @@ func publishTransformPolicyCases() []publishTransformPolicyCase {
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "source only enabled transform rejected",
|
||||
publish: PublishPolicy{Source: true},
|
||||
transform: Transform{MarkdownToHTML: &MarkdownToHTML{
|
||||
Enabled: true,
|
||||
Mode: TransformModeSidecar,
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "enabled markdown wrong mode rejected",
|
||||
publish: PublishPolicy{Source: true},
|
||||
@@ -136,6 +169,16 @@ func publishTransformPolicyCases() []publishTransformPolicyCase {
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "sidecar input rejected",
|
||||
publish: PublishPolicy{HTML: true},
|
||||
transform: Transform{MarkdownToHTML: &MarkdownToHTML{
|
||||
Enabled: true,
|
||||
Mode: TransformModeSidecar,
|
||||
Input: "report.md",
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "disabled markdown empty mode allowed",
|
||||
publish: PublishPolicy{Source: true},
|
||||
@@ -151,6 +194,24 @@ func publishTransformPolicyCases() []publishTransformPolicyCase {
|
||||
Mode: TransformModeSidecar,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "disabled markdown index mode allowed",
|
||||
publish: PublishPolicy{Source: true},
|
||||
transform: Transform{MarkdownToHTML: &MarkdownToHTML{
|
||||
Enabled: false,
|
||||
Mode: TransformModeIndex,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "disabled markdown input rejected",
|
||||
publish: PublishPolicy{Source: true},
|
||||
transform: Transform{MarkdownToHTML: &MarkdownToHTML{
|
||||
Enabled: false,
|
||||
Mode: TransformModeIndex,
|
||||
Input: "report.md",
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "disabled markdown wrong mode rejected",
|
||||
publish: PublishPolicy{Source: true},
|
||||
|
||||
@@ -26,6 +26,10 @@ func PlanOutputs(ctx context.Context, req Request) ([]Output, error) {
|
||||
generatedOutputs, err := transformer.Generate(ctx, transform.Request{
|
||||
SourceBundle: req.SourceBundle,
|
||||
SourceBackend: req.SourceBackend,
|
||||
Markdown: transform.MarkdownOptions{
|
||||
Mode: req.Transform.MarkdownToHTML.Mode,
|
||||
Input: req.Transform.MarkdownToHTML.Input,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -103,6 +103,35 @@ func TestPlanOutputsUsesRegisteredTransformer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanOutputsPassesMarkdownOptions(t *testing.T) {
|
||||
data := []byte("<p>Generated</p>\n")
|
||||
transformer := &recordingTransformer{outputs: []transform.Output{{
|
||||
Path: "index.html",
|
||||
SourcePath: "report.md",
|
||||
Transform: transform.MarkdownToHTML,
|
||||
Data: data,
|
||||
SHA256: bundle.FileDigest(data),
|
||||
Size: int64(len(data)),
|
||||
}}}
|
||||
|
||||
_, err := PlanOutputs(context.Background(), Request{
|
||||
Publish: config.PublishPolicy{HTML: true},
|
||||
Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{
|
||||
Enabled: true,
|
||||
Mode: config.TransformModeIndex,
|
||||
Input: "report.md",
|
||||
}},
|
||||
Transformers: testResolver{transform.MarkdownToHTML: transformer},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("PlanOutputs() error = %v", err)
|
||||
}
|
||||
if transformer.request.Markdown.Mode != config.TransformModeIndex || transformer.request.Markdown.Input != "report.md" {
|
||||
t.Fatalf("markdown options = %#v, want index/report.md", transformer.request.Markdown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRejectsHTMLWithoutTransform(t *testing.T) {
|
||||
sourceBackend := fake.New()
|
||||
destinationBackend := fake.New()
|
||||
@@ -148,6 +177,14 @@ func TestValidateRequestChecksPublishTransformPolicy(t *testing.T) {
|
||||
Mode: config.TransformModeSidecar,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "html only index allowed",
|
||||
publish: config.PublishPolicy{HTML: true},
|
||||
transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{
|
||||
Enabled: true,
|
||||
Mode: config.TransformModeIndex,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "source and html sidecar allowed",
|
||||
publish: config.PublishPolicy{Source: true, HTML: true},
|
||||
@@ -184,6 +221,15 @@ func TestValidateRequestChecksPublishTransformPolicy(t *testing.T) {
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "source only enabled transform rejected",
|
||||
publish: config.PublishPolicy{Source: true},
|
||||
transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{
|
||||
Enabled: true,
|
||||
Mode: config.TransformModeSidecar,
|
||||
}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "enabled markdown wrong mode rejected",
|
||||
publish: config.PublishPolicy{Source: true},
|
||||
@@ -253,3 +299,14 @@ type testTransformer struct {
|
||||
func (t testTransformer) Generate(context.Context, transform.Request) ([]transform.Output, error) {
|
||||
return t.outputs, t.err
|
||||
}
|
||||
|
||||
type recordingTransformer struct {
|
||||
outputs []transform.Output
|
||||
request transform.Request
|
||||
err error
|
||||
}
|
||||
|
||||
func (t *recordingTransformer) Generate(_ context.Context, req transform.Request) ([]transform.Output, error) {
|
||||
t.request = req
|
||||
return t.outputs, t.err
|
||||
}
|
||||
|
||||
@@ -25,24 +25,26 @@ func (t *Transformer) Generate(ctx context.Context, req transform.Request) ([]tr
|
||||
if t.renderer == nil {
|
||||
t.renderer = goldmark.New()
|
||||
}
|
||||
switch markdownMode(req.Markdown.Mode) {
|
||||
case transform.MarkdownModeSidecar:
|
||||
return t.generateSidecars(ctx, req)
|
||||
case transform.MarkdownModeIndex:
|
||||
return t.generateIndex(ctx, req)
|
||||
default:
|
||||
return nil, fmt.Errorf("markdown mode %q is not supported", req.Markdown.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transformer) generateSidecars(ctx context.Context, req transform.Request) ([]transform.Output, error) {
|
||||
var outputs []transform.Output
|
||||
for _, file := range req.SourceBundle.Manifest.Files {
|
||||
if !strings.HasSuffix(file.Path, ".md") {
|
||||
continue
|
||||
}
|
||||
sourcePath, err := storage.Join(req.SourceBundle.RootRelativePath, file.Path)
|
||||
html, err := t.render(ctx, req, file.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := req.SourceBackend.ReadFile(ctx, sourcePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read markdown source %q: %w", file.Path, err)
|
||||
}
|
||||
var rendered bytes.Buffer
|
||||
if err := t.renderer.Convert(data, &rendered); err != nil {
|
||||
return nil, fmt.Errorf("render markdown source %q: %w", file.Path, err)
|
||||
}
|
||||
html := wrapHTML(rendered.Bytes())
|
||||
outputPath := strings.TrimSuffix(file.Path, ".md") + ".html"
|
||||
outputs = append(outputs, transform.Output{
|
||||
Path: outputPath,
|
||||
@@ -55,3 +57,78 @@ func (t *Transformer) Generate(ctx context.Context, req transform.Request) ([]tr
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
func (t *Transformer) generateIndex(ctx context.Context, req transform.Request) ([]transform.Output, error) {
|
||||
input, err := selectIndexInput(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
html, err := t.render(ctx, req, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []transform.Output{{
|
||||
Path: "index.html",
|
||||
SourcePath: input,
|
||||
Transform: transform.MarkdownToHTML,
|
||||
Data: html,
|
||||
SHA256: bundle.FileDigest(html),
|
||||
Size: int64(len(html)),
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func selectIndexInput(req transform.Request) (string, error) {
|
||||
if req.Markdown.Input != "" {
|
||||
if err := storage.ValidatePath(req.Markdown.Input); err != nil {
|
||||
return "", fmt.Errorf("markdown input %q: %w", req.Markdown.Input, err)
|
||||
}
|
||||
for _, file := range req.SourceBundle.Manifest.Files {
|
||||
if file.Path != req.Markdown.Input {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(file.Path, ".md") {
|
||||
return "", fmt.Errorf("markdown input %q must end in .md", req.Markdown.Input)
|
||||
}
|
||||
return file.Path, nil
|
||||
}
|
||||
return "", fmt.Errorf("markdown input %q is not listed in the source manifest", req.Markdown.Input)
|
||||
}
|
||||
|
||||
var markdownFiles []string
|
||||
for _, file := range req.SourceBundle.Manifest.Files {
|
||||
if strings.HasSuffix(file.Path, ".md") {
|
||||
markdownFiles = append(markdownFiles, file.Path)
|
||||
}
|
||||
}
|
||||
switch len(markdownFiles) {
|
||||
case 0:
|
||||
return "", fmt.Errorf("markdown index mode requires one markdown source file or transform.markdown_to_html.input")
|
||||
case 1:
|
||||
return markdownFiles[0], nil
|
||||
default:
|
||||
return "", fmt.Errorf("markdown index mode found multiple markdown source files; set transform.markdown_to_html.input")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transformer) render(ctx context.Context, req transform.Request, sourceFile string) ([]byte, error) {
|
||||
sourcePath, err := storage.Join(req.SourceBundle.RootRelativePath, sourceFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := req.SourceBackend.ReadFile(ctx, sourcePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read markdown source %q: %w", sourceFile, err)
|
||||
}
|
||||
var rendered bytes.Buffer
|
||||
if err := t.renderer.Convert(data, &rendered); err != nil {
|
||||
return nil, fmt.Errorf("render markdown source %q: %w", sourceFile, err)
|
||||
}
|
||||
return wrapHTML(rendered.Bytes()), nil
|
||||
}
|
||||
|
||||
func markdownMode(mode string) string {
|
||||
if mode == "" {
|
||||
return transform.MarkdownModeSidecar
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
@@ -40,6 +40,124 @@ func TestGenerateMarkdownSidecar(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMarkdownIndexExplicitInput(t *testing.T) {
|
||||
backend := fake.New()
|
||||
sourceBundle := testutil.WriteFakeSourceBundle(t, backend, "", testutil.BundleOptions{
|
||||
ID: "bundle",
|
||||
Files: []testutil.SourceFile{
|
||||
{Path: "report.md", Data: "# Report\n"},
|
||||
{Path: "notes.md", Data: "# Notes\n"},
|
||||
},
|
||||
})
|
||||
|
||||
outputs, err := New().Generate(context.Background(), transform.Request{
|
||||
SourceBackend: backend,
|
||||
SourceBundle: sourceBundle,
|
||||
Markdown: transform.MarkdownOptions{Mode: transform.MarkdownModeIndex, Input: "notes.md"},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error = %v", err)
|
||||
}
|
||||
if got, want := len(outputs), 1; got != want {
|
||||
t.Fatalf("output count = %d, want %d", got, want)
|
||||
}
|
||||
output := outputs[0]
|
||||
if output.Path != "index.html" || output.SourcePath != "notes.md" || output.Transform != transform.MarkdownToHTML {
|
||||
t.Fatalf("output metadata = %#v, want index from notes.md", output)
|
||||
}
|
||||
if !strings.Contains(string(output.Data), "<h1>Notes</h1>") {
|
||||
t.Fatalf("html = %q, want notes content", output.Data)
|
||||
}
|
||||
if output.SHA256 != bundle.FileDigest(output.Data) || output.Size != int64(len(output.Data)) {
|
||||
t.Fatalf("digest/size metadata = %s/%d", output.SHA256, output.Size)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMarkdownIndexSelectsOnlyMarkdownFile(t *testing.T) {
|
||||
backend, sourceBundle := markdownFixture(t, "# Title\n\nHello.\n")
|
||||
|
||||
outputs, err := New().Generate(context.Background(), transform.Request{
|
||||
SourceBackend: backend,
|
||||
SourceBundle: sourceBundle,
|
||||
Markdown: transform.MarkdownOptions{Mode: transform.MarkdownModeIndex},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error = %v", err)
|
||||
}
|
||||
if got, want := outputs[0].Path, "index.html"; got != want {
|
||||
t.Fatalf("path = %q, want %q", got, want)
|
||||
}
|
||||
if got, want := outputs[0].SourcePath, "report.md"; got != want {
|
||||
t.Fatalf("source path = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMarkdownIndexRejectsAmbiguousInput(t *testing.T) {
|
||||
backend := fake.New()
|
||||
sourceBundle := testutil.WriteFakeSourceBundle(t, backend, "", testutil.BundleOptions{
|
||||
ID: "bundle",
|
||||
Files: []testutil.SourceFile{
|
||||
{Path: "report.md", Data: "# Report\n"},
|
||||
{Path: "notes.md", Data: "# Notes\n"},
|
||||
},
|
||||
})
|
||||
|
||||
_, err := New().Generate(context.Background(), transform.Request{
|
||||
SourceBackend: backend,
|
||||
SourceBundle: sourceBundle,
|
||||
Markdown: transform.MarkdownOptions{Mode: transform.MarkdownModeIndex},
|
||||
})
|
||||
|
||||
if err == nil || !strings.Contains(err.Error(), "multiple markdown source files") {
|
||||
t.Fatalf("Generate() error = %v, want ambiguous input error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMarkdownIndexRejectsMissingMarkdown(t *testing.T) {
|
||||
backend := fake.New()
|
||||
sourceBundle := testutil.WriteFakeSourceBundle(t, backend, "", testutil.BundleOptions{
|
||||
ID: "bundle",
|
||||
Files: []testutil.SourceFile{{Path: "summary.txt", Data: "Summary\n"}},
|
||||
})
|
||||
|
||||
_, err := New().Generate(context.Background(), transform.Request{
|
||||
SourceBackend: backend,
|
||||
SourceBundle: sourceBundle,
|
||||
Markdown: transform.MarkdownOptions{Mode: transform.MarkdownModeIndex},
|
||||
})
|
||||
|
||||
if err == nil || !strings.Contains(err.Error(), "requires one markdown source file") {
|
||||
t.Fatalf("Generate() error = %v, want missing markdown error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMarkdownIndexRejectsInvalidExplicitInput(t *testing.T) {
|
||||
backend, sourceBundle := markdownFixture(t, "# Title\n")
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantError string
|
||||
}{
|
||||
{name: "unsafe", input: "../report.md", wantError: "markdown input"},
|
||||
{name: "not listed", input: "missing.md", wantError: "not listed"},
|
||||
{name: "not markdown", input: "summary.txt", wantError: "must end in .md"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := New().Generate(context.Background(), transform.Request{
|
||||
SourceBackend: backend,
|
||||
SourceBundle: sourceBundle,
|
||||
Markdown: transform.MarkdownOptions{Mode: transform.MarkdownModeIndex, Input: tt.input},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantError) {
|
||||
t.Fatalf("Generate() error = %v, want substring %q", err, tt.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateIgnoresNonMarkdown(t *testing.T) {
|
||||
backend := fake.New()
|
||||
if _, err := backend.WriteFile(context.Background(), "summary.txt", []byte("Summary\n"), storage.WriteOptions{}); err != nil {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
package transform
|
||||
|
||||
const MarkdownToHTML = "markdown_to_html"
|
||||
|
||||
const (
|
||||
MarkdownModeSidecar = "sidecar"
|
||||
MarkdownModeIndex = "index"
|
||||
)
|
||||
|
||||
@@ -19,6 +19,12 @@ type Output struct {
|
||||
type Request struct {
|
||||
SourceBundle bundle.Bundle
|
||||
SourceBackend storage.Backend
|
||||
Markdown MarkdownOptions
|
||||
}
|
||||
|
||||
type MarkdownOptions struct {
|
||||
Mode string
|
||||
Input string
|
||||
}
|
||||
|
||||
type Transformer interface {
|
||||
|
||||
Reference in New Issue
Block a user