Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support string-based attributes #9

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
*.pdf
*_files/
/.luarc.json

/.quarto/
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Filter to include code from source files.

The filter is largely inspired by
[pandoc-include-code](https://github.com/owickstrom/pandoc-include-code).
[pandoc-include-code](https://github.com/owickstrom/pandoc-include-code) and [sphinx-literalinclude](https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-literalinclude).

## Installing

Expand Down Expand Up @@ -41,18 +41,47 @@ You can still use other attributes, and classes, to control the code blocks:
```{.python include="script.py" code-line-numbers="true"}
```

### Dedent

Using the `dedent` attribute, you can have whitespaces removed on each line, where possible (non-whitespace character will not be removed even if they occur
in the dedent area).

```{.python include="script.py" dedent=4}
```

### Ranges

If you want to include a specific range of lines, use `start-line` and `end-line`:

```{.python include="script.py" start-line=35 end-line=80}
```

### Dedent
#### New in Version 1.1

Using the `dedent` attribute, you can have whitespaces removed on each line, where possible (non-whitespace character will not be removed even if they occur
in the dedent area).
`include-code-files` now supports additional attributes to specify ranges:

```{.python include="script.py" dedent=4}
* `start-after`: Start immediately after the specified line
* `end-before`: End immediately before the specified line

Furthermore, all range attributes (including `start-line` and `end-line`) now support both numeric and string values.

Using string comments in code, in combination with the `start-after` and `end-before` range attributes allows for
editing of the include file without requiring you to re-determine the proper numeric values after the changes are complete.

For example, in this python file:

```python
# [my_method start]
def my_method():
print("do work")
# [my_method end]
```

To include just the method in your code block, you can do the following:

```{.python include="script.py" start-after="[my_method start]" end-after="[my_method end]"}
```

Then as you edit your method, only the lines between `[my_method start]` and `[my_method end]` will be included in the output.

Note that any combination of start and end attributes is supported. See the test setup in `index.qmd` for more examples.
4 changes: 1 addition & 3 deletions _extensions/include-code-files/_extension.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
title: Include Code Files
author: Bruno Beaufils
version: 1.0.0
version: 1.1.0
quarto-required: ">=1.2"
contributes:
filters:
- include-code-files.lua


144 changes: 106 additions & 38 deletions _extensions/include-code-files/include-code-files.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,54 +10,122 @@ end

--- Filter function for code blocks
local function transclude (cb)
local content = ""

if cb.attributes.include then
local content = ""
local fh = io.open(cb.attributes.include)
if not fh then
io.stderr:write("Cannot open file " .. cb.attributes.include .. " | Skipping includes\n")
goto cleanup
end

-- change hyphenated attributes to PascalCase
for i,pascal in pairs({"startLine", "endLine", "startAfter", "endBefore"})
do
local hyphen = pascal:gsub("%u", "-%0"):lower()
if cb.attributes[hyphen] then
cb.attributes[pascal] = cb.attributes[hyphen]
cb.attributes[hyphen] = nil
end
end

if cb.attributes.startLine and cb.attributes.startAfter then
io.stderr:write("Cannot specify both startLine and startAfter | Skipping includes\n")
goto cleanup
end

if cb.attributes.endLine and cb.attributes.endBefore then
io.stderr:write("Cannot specify both endLine and endBefore | Skipping includes\n")
goto cleanup
end

local number = 0
local start = nil
local finish = nil
local skipFirst = false
local skipLast = false

-- set start and skipFirst based on start params
if cb.attributes.startLine then
start = tonumber(cb.attributes.startLine)
if not start then
start = cb.attributes.startLine
end
elseif cb.attributes.startAfter then
start = tonumber(cb.attributes.startAfter)
if not start then
start = cb.attributes.startAfter
end
skipFirst = true
else
local number = 1
local start = 1

-- change hyphenated attributes to PascalCase
for i,pascal in pairs({"startLine", "endLine"})
do
local hyphen = pascal:gsub("%u", "-%0"):lower()
if cb.attributes[hyphen] then
cb.attributes[pascal] = cb.attributes[hyphen]
cb.attributes[hyphen] = nil
end
end

if cb.attributes.startLine then
cb.attributes.startFrom = cb.attributes.startLine
start = tonumber(cb.attributes.startLine)
end
for line in fh:lines ("L")
do
if cb.attributes.dedent then
line = dedent(line, cb.attributes.dedent)
-- if no start specified, start at the first line
start = 1
end

-- set finish and skipLast based on end params
if cb.attributes.endLine then
finish = tonumber(cb.attributes.endLine)
if not finish then
finish = cb.attributes.endLine
end
elseif cb.attributes.endBefore then
finish = tonumber(cb.attributes.endBefore)
if not finish then
finish = cb.attributes.endBefore
end
skipLast = true
else
-- if no end specified, end at the last line
end

for line in fh:lines ("L")
do
number = number + 1
-- if start or finish is a string, check if it exists on the current line
if type(start) == "string" and string.find(line, start, 1, true) then
start = number
elseif type(finish) == "string" and string.find(line, finish, 1 , true) then
finish = number
end

-- if haven't found start yet, then continue
if start and type(start) == "number" then
if number < start or (number == start and skipFirst) then
goto continue
end
if number >= start then
if not cb.attributes.endLine or number <= tonumber(cb.attributes.endLine) then
content = content .. line
end
elseif start then
-- else if start is still a string, then continue
goto continue
end

-- if found finish, then end
if finish and type(finish) == "number" then
if number > finish or (number == finish and skipLast) then
break
end
number = number + 1
end
fh:close()
end
-- remove key-value pair for used keys
cb.attributes.include = nil
cb.attributes.startLine = nil
cb.attributes.endLine = nil
cb.attributes.dedent = nil
-- return final code block
return pandoc.CodeBlock(content, cb.attr)
end

if cb.attributes.dedent then
line = dedent(line, cb.attributes.dedent)
end

content = content .. line
::continue::
end
fh:close()
end
::cleanup::
-- remove key-value pair for used keys
cb.attributes.include = nil
cb.attributes.startLine = nil
cb.attributes.startAfter = nil
cb.attributes.endLine = nil
cb.attributes.endBefore = nil
cb.attributes.dedent = nil
-- return final code block
return pandoc.CodeBlock(content, cb.attr)
end

return {
{ CodeBlock = transclude }
}

9 changes: 9 additions & 0 deletions _quarto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
project:
type: website
output-dir: .quarto
preview:
browser: true
watch-inputs: true

website:
title: "Quarto + Include-Code-Files"
67 changes: 67 additions & 0 deletions index.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: Test of Include-Code-Files Quarto Extension
filters:
- include-code-files
---

**In this post, we will show a simple implementation of `Tic-Tac-Toe` generated by ChatGPT4**

Here is the initial class definition:

```{.python include="tic_tac_toe.py" end-before="[TicTacToe init start]"}
```


Here is a short description of each method within the TicTacToe class in the provided Python file.


1. `__init__(self)`: The constructor for the TicTacToe class. It initializes the game board as a 3x3 grid of spaces and sets the current player to 'X'.

```{.python include="tic_tac_toe.py" dedent=4 start-after="[TicTacToe init start]" end-before="[TicTacToe init end]"}
```

1. `print_board(self)`: Prints the current state of the game board to the console, including the grid lines.

```{.python include="tic_tac_toe.py" dedent=4 start-line="def print_board(self)" end-line=13}
```

1. `is_valid_move(self, row, col)`: Checks whether the specified move (by row and column indices) is valid; that is, if the chosen cell on the board is empty (' ').

```{.python include="tic_tac_toe.py" dedent=4 start-line=15 end-line=16}
```

1. `place_mark(self, row, col)`: Places the current player's mark ('X' or 'O') on the board at the specified location if the move is valid, and returns False if the move is invalid (i.e., if the spot is already taken).

```{.python include="tic_tac_toe.py" dedent=4 start-line=18 end-before=23}
```

1. `switch_player(self)`: Switches the current player from 'X' to 'O' or 'O' to 'X', toggling back and forth after each valid move.

```{.python include="tic_tac_toe.py" dedent=4 start-after=23 end-line=25}
```

1. `check_winner(self)`: Checks all possible winning combinations (rows, columns, and diagonals) to see if either player has won the game. It returns the winning player's mark ('X' or 'O') if there is a winner, or None if there isn't one yet.

```{.python include="tic_tac_toe.py" dedent=4 start-line=27 end-line="return None"}
```

1. `is_board_full(self)`: Checks whether the board is completely filled with players' marks; returns True if full, indicating a tie if there's no winner, or False if there are still empty spaces.

```{.python include="tic_tac_toe.py" dedent=4 start-line=40 end-line=41}
```

1. `play_game(self)`: The main game loop that repeatedly asks the current player for their move, checks for a win or a tie, and switches players. This method controls the game flow, displaying the board and prompting the players until the game ends with a winner or a tie.

```{.python include="tic_tac_toe.py" dedent=4 start-line="def play_game(self)" end-before="# Main game execution"}
```

Here is the main game execution:

```{.python include="tic_tac_toe.py" start-after="# Main game execution"}
```


Finally, here is the full implementation:

```{.python include="tic_tac_toe.py" code-line-numbers="true"}
```
78 changes: 78 additions & 0 deletions tic_tac_toe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# tic_tac_toe.py

class TicTacToe:
# [TicTacToe init start]
def __init__(self):
self.board = [[' ' for _ in range(3)] for _ in range(3)]
self.current_turn = 'X'
# [TicTacToe init end]

def print_board(self):
for row in self.board:
print('|'.join(row))
print('-'*5)

def is_valid_move(self, row, col):
return self.board[row][col] == ' '

def place_mark(self, row, col):
if not self.is_valid_move(row, col):
return False
self.board[row][col] = self.current_turn
return True

def switch_player(self):
self.current_turn = 'O' if self.current_turn == 'X' else 'X'

def check_winner(self):
# Check rows, columns and diagonals
for i in range(3):
if self.board[i][0] == self.board[i][1] == self.board[i][2] != ' ':
return self.board[i][0]
if self.board[0][i] == self.board[1][i] == self.board[2][i] != ' ':
return self.board[0][i]
if self.board[0][0] == self.board[1][1] == self.board[2][2] != ' ':
return self.board[0][0]
if self.board[0][2] == self.board[1][1] == self.board[2][0] != ' ':
return self.board[0][2]
return None

def is_board_full(self):
return all(self.board[row][col] != ' ' for row in range(3) for col in range(3))

def play_game(self):
while True:
self.print_board()

# Try to place a mark, if the move is invalid, retry.
try:
row = int(input(f"Player {self.current_turn}, enter your move row (0-2): "))
col = int(input(f"Player {self.current_turn}, enter your move column (0-2): "))
except ValueError:
print("Please enter numbers between 0 and 2.")
continue

if row < 0 or row > 2 or col < 0 or col > 2:
print("Invalid move. Try again.")
continue

if not self.place_mark(row, col):
print("This spot is taken. Try another spot.")
continue

winner = self.check_winner()
if winner:
self.print_board()
print(f"Player {winner} wins!")
break
elif self.is_board_full():
self.print_board()
print("It's a tie!")
break

self.switch_player()

# Main game execution
if __name__ == "__main__":
game = TicTacToe()
game.play_game()