Skip to content
Open
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
14 changes: 14 additions & 0 deletions doc/content/task-syntax/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ echo "Hello ертөнц!"
#+end_src
```

## Indented code blocks

A script can also be written as an indented code block, using four spaces or a tab
instead of a ``` fence. This is handy when the surrounding document already uses
fenced blocks for something else.

````markdown
## Tasks
### Task1

echo "Hello 世界!"
echo "Hello العالمية!"
````

## Shebangs

To define an alternative interpreter such as python, then include a shebang, similar to the unix style.
Expand Down
40 changes: 40 additions & 0 deletions parser/parsemd/parsemd.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var ErrNoTasksHeading = errors.New("no xc block found")
const (
trimValues = "_*` "
codeBlockStarter = "```"
indentedCodeIndent = " "
defaultHeading = "tasks"
headingMarkerComment = "<!-- xc-heading -->"
)
Expand Down Expand Up @@ -244,6 +245,42 @@ func (p *parser) parseCodeBlock() error {
return nil
}

func isIndentedCode(line string) bool {
return strings.HasPrefix(line, indentedCodeIndent) || strings.HasPrefix(line, "\t")
}

func trimCodeIndent(line string) string {
if strings.HasPrefix(line, "\t") {
return line[1:]
}
return strings.TrimPrefix(line, indentedCodeIndent)
}

// parseIndentedCodeBlock handles a script written as an indented code block,
// where each line is indented with four spaces or a tab instead of being
// wrapped in a ``` fence. A blank line does not end the block if more indented
// code follows it, so chunks separated by a blank line are treated as one
// script, the same way a fenced block ignores blank lines.
func (p *parser) parseIndentedCodeBlock() error {
if !isIndentedCode(p.currentLine) {
return nil
}
if len(p.currTask.Script) > 0 {
return fmt.Errorf("command block already exists for task %s", p.currTask.Name)
}
for {
if isIndentedCode(p.currentLine) {
p.currTask.Script += trimCodeIndent(p.currentLine) + "\n"
} else if strings.TrimSpace(p.currentLine) != "" {
break
}
if !p.scan() {
break
}
}
return nil
}

func (p *parser) findTaskHeading() (heading string, done bool, err error) {
for {
tok, level, text, markerFound := p.parseHeading(true)
Expand Down Expand Up @@ -283,6 +320,9 @@ func (p *parser) parseTaskBody() (bool, error) {
if err != nil {
return false, err
}
if err = p.parseIndentedCodeBlock(); err != nil {
return false, err
}
tok, level, _, _ := p.parseHeading(false)
if tok && level <= p.rootHeadingLevel {
return false, nil
Expand Down
58 changes: 58 additions & 0 deletions parser/parsemd/parsemd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ var tillEOF string
//go:embed testdata/notasks.md
var e string

//go:embed testdata/indented.md
var indented string

func assertTask(t *testing.T, expected, actual models.Task) {
t.Helper()
if expected.Name != actual.Name {
Expand Down Expand Up @@ -282,6 +285,61 @@ some code
}
}

func TestParseIndentedScripts(t *testing.T) {
p, err := NewParser(strings.NewReader(indented), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
result, err := p.Parse()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := models.Tasks{
{Name: "list", Description: []string{"Lists files"}, Script: "ls\n"},
{
Name: "greet",
Description: []string{"Print a message"},
Script: "echo \"Hello, world!\"\necho \"Hello, world2!\"\n",
},
}
if len(result) != len(expected) {
t.Fatalf("want %d tasks got %d", len(expected), len(result))
}
for i, exp := range expected {
assertTask(t, exp, result[i])
}
}

func TestIndentedBlockSeparatedByBlankLine(t *testing.T) {
p, _ := NewParser(strings.NewReader(`
# Tasks
## a task
echo one

echo two
`), nil)
_, err := p.parseTask()
if err != nil {
t.Fatal(err)
}
want := "echo one\necho two\n"
if p.currTask.Script != want {
t.Fatalf("script want=%q got=%q", want, p.currTask.Script)
}
}

func TestIndentedBlockAfterFencedBlock(t *testing.T) {
var p parser
p.scanner = bufio.NewScanner(strings.NewReader(" echo hi"))
p.scan()
p.scan()
p.currTask.Script = "an existing script"
err := p.parseIndentedCodeBlock()
if err == nil {
t.Fatal("expected error got nil")
}
}

func TestMultipleCodeBlocks(t *testing.T) {
var p parser
p.scanner = bufio.NewScanner(strings.NewReader("```\ncode\n```"))
Expand Down
16 changes: 16 additions & 0 deletions parser/parsemd/testdata/indented.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# My readme

## Tasks

### list

Lists files

ls

### greet

Print a message

echo "Hello, world!"
echo "Hello, world2!"