Skip to content

Commit c7f3e95

Browse files
refactor codeAsInSource to not require command + fix nbJs bug fixes (#163)
* refactor codeAsInSource to not require command * fix infix * add more fixes * implement nb.sourceFiles * fix typesection and add discard test * fix for loops * let try pietro's idea of taking the minimum * fix parallel bug * add template test * fix type bug * update changelog * bump nimble version * clean sources * add Hugo as co-author * add filename assert in Pos comparision * assert -> doAssert. Improved error message and added an assert for the endPos as well
1 parent ad0e44b commit c7f3e95

File tree

6 files changed

+145
-152
lines changed

6 files changed

+145
-152
lines changed

changelog.md

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ When contributing a fix, feature or example please add a new line to briefly exp
1010
## 0.3.x
1111
* _add next change here_
1212

13+
## 0.3.5
14+
* codeAsInSource has been reworked to work better with templates and uses of `nbCode` in different files.
15+
* If you don't use any nbJs, now nimib won't build an empty nim file in those cases.
16+
* The temporary js files generated by nbJs now has unique names to allow parallel builds.
17+
1318
## 0.3.4
1419

1520
* added `nbCodeDisplay` and `nbCodeAnd` (#158).

nimib.nimble

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Package
22

3-
version = "0.3.4"
4-
author = "Pietro Peterlongo"
3+
version = "0.3.5"
4+
author = "Pietro Peterlongo & Hugo Granström"
55
description = "nimib 🐳 - nim 👑 driven ⛵ publishing ✍"
66
license = "MIT"
77
srcDir = "src"

src/nimib/jsutils.nim

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import std / [macros, macrocache, tables, strutils, strformat, sequtils, sugar, os]
1+
import std / [macros, macrocache, tables, strutils, strformat, sequtils, sugar, os, hashes]
22
import ./types
33

44
proc contains(tab: CacheTable, keyToCheck: string): bool =
@@ -142,7 +142,7 @@ proc compileNimToJs*(doc: var NbDoc, blk: var NbBlock) =
142142
createDir(tempdir)
143143
let (dir, filename, ext) = doc.thisFile.splitFile()
144144
let nimfile = dir / (filename & "_nbCodeToJs_" & $doc.newId() & ext).RelativeFile
145-
let jsfile = tempdir / "out.js"
145+
let jsfile = tempdir / &"out{hash(doc.thisFile)}.js"
146146
var codeText = blk.context["transformedCode"].vString
147147
let nbJsCounter = doc.nbJsCounter
148148
doc.nbJsCounter += 1
@@ -178,8 +178,9 @@ proc nbCollectAllNbJs*(doc: var NbDoc) =
178178
code.add "\n" & blk.context["transformedCode"].vString
179179
code = topCode & "\n" & code
180180

181-
# Create block which which will compile the code when rendered (nbJsFromJsOwnFile)
182-
var blk = NbBlock(command: "nbJsFromCodeOwnFile", code: code, context: newContext(searchDirs = @[], partials = doc.partials), output: "")
183-
blk.context["transformedCode"] = code
184-
doc.blocks.add blk
185-
doc.blk = blk
181+
if not code.isEmptyOrWhitespace:
182+
# Create block which which will compile the code when rendered (nbJsFromJsOwnFile)
183+
var blk = NbBlock(command: "nbJsFromCodeOwnFile", code: code, context: newContext(searchDirs = @[], partials = doc.partials), output: "")
184+
blk.context["transformedCode"] = code
185+
doc.blocks.add blk
186+
doc.blk = blk

src/nimib/sources.nim

+72-142
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,40 @@ import std/[
66
import types
77

88

9-
109
# Credits to @haxscramper for sharing his code on reading the line info
1110
# And credits to @Yardanico for making a previous attempt which @hugogranstrom have taken much inspiration from
1211
# when implementing this.
1312

1413
type
1514
Pos* = object
15+
filename*: string
1616
line*: int
1717
column*: int
1818

19+
proc `<`*(p1, p2: Pos): bool =
20+
doAssert p1.filename == p2.filename, """
21+
Code from two different files were found in the same nbCode!
22+
If you want to mix code from different files in nbCode, use -d:nimibCodeFromAst instead.
23+
If you are not mixing code from different files, please open an issue on nimib's Github with a minimal reproducible example."""
24+
(p1.line, p1.column) < (p2.line, p2.column)
25+
1926
proc toPos*(info: LineInfo): Pos =
20-
Pos(line: info.line, column: info.column)
27+
Pos(line: info.line, column: info.column, filename: info.filename)
28+
29+
proc startPos(node: NimNode): Pos =
30+
case node.kind
31+
of nnkStmtList:
32+
return node[0].startPos()
33+
else:
34+
result = toPos(node.lineInfoObj())
35+
for child in node.children:
36+
let childPos = child.startPos()
37+
# If we can't get the line info for some reason, skip it!
38+
if childPos.line == 0: continue
39+
40+
if childPos < result:
41+
result = childPos
2142

22-
proc startPos*(node: NimNode): Pos =
23-
## Get the starting position of a NimNode. Corrections will be needed for certains cases though.
24-
# Has column info
25-
case node.kind:
26-
of nnkNone .. nnkNilLit, nnkDiscardStmt, nnkCommentStmt:
27-
result = toPos(node.lineInfoObj())
28-
of nnkBlockStmt:
29-
result = node[1].startPos()
30-
else:
31-
result = node[0].startPos()
3243

3344
proc finishPos*(node: NimNode): Pos =
3445
## Get the ending position of a NimNode. Corrections will be needed for certains cases though.
@@ -56,18 +67,24 @@ proc finishPos*(node: NimNode): Pos =
5667
proc isCommandLine*(s: string, command: string): bool =
5768
nimIdentNormalize(s.strip()).startsWith(nimIdentNormalize(command))
5869

70+
proc isCommentLine*(s: string): bool =
71+
s.strip.startsWith('#')
72+
73+
proc findStartLine*(source: seq[string], startPos: Pos): int =
74+
let line = source[startPos.line - 1]
75+
let preline = line[0 ..< startPos.column - 1]
76+
# Multiline, we need to check further up for comments
77+
if preline.isEmptyOrWhitespace:
78+
result = startPos.line - 1
79+
# Make sure we catch all comments
80+
while source[result-1].isCommentLine() or source[result-1].isEmptyOrWhitespace() or source[result-1].nimIdentNormalize.strip() == "type":
81+
dec result
82+
# Now remove all empty lines
83+
while source[result].isEmptyOrWhitespace():
84+
inc result
85+
else: # starts on same line as command
86+
return startPos.line - 1
5987

60-
proc findStartLine*(source: seq[string], command: string, startPos: int): int =
61-
if source[startPos].isCommandLine(command):
62-
return startPos
63-
# The code is starting on a line below the command
64-
# Decrease result until it is on the line below the command
65-
result = startPos
66-
while not source[result-1].isCommandLine(command):
67-
dec result
68-
# Remove empty lines at the beginning of the block
69-
while source[result].isEmptyOrWhitespace:
70-
inc result
7188

7289
proc findEndLine*(source: seq[string], command: string, startLine, endPos: int): int =
7390
result = endPos
@@ -90,49 +107,32 @@ proc findEndLine*(source: seq[string], command: string, startLine, endPos: int):
90107
while result < source.high and (source[result+1].startsWith(baseIndentStr) or source[result+1].isEmptyOrWhitespace):
91108
inc result
92109

93-
94110
proc getCodeBlock*(source, command: string, startPos, endPos: Pos): string =
95111
## Extracts the code in source from startPos to endPos with additional processing to get the entire code block.
96-
let rawLines = source.split("\n")
97-
var startLine = findStartLine(rawLines, command, startPos.line - 1)
112+
let rawLines = source.splitLines()
113+
let rawStartLine = startPos.line - 1
114+
let rawStartCol = startPos.column - 1
115+
var startLine = findStartLine(rawLines, startPos)
98116
var endLine = findEndLine(rawLines, command, startLine, endPos.line - 1)
99117

100118
var lines = rawLines[startLine .. endLine]
101119

102-
let baseIndent = skipWhile(rawLines[startLine], {' '})
103-
104-
let startsOnCommandLine = lines[0].isCommandLine(command) # is it nbCode: code or nbCode: <enter> code
105-
if startsOnCommandLine: # remove the command
106-
var startColumn = startPos.column
107-
# the "import"-part is not included in the startPos
108-
let startsWithImport = lines[0].find("import")
109-
if startsWithImport != -1:
110-
startColumn = startsWithImport
111-
lines[0] = lines[0][startColumn .. ^1].strip()
112-
113-
var codeText: string
114-
if startLine == endLine and startsOnCommandLine: # single-line expression
115-
# remove eventual unneccerary parenthesis
116-
let line = rawLines[startLine] # includes command and eventual opening parethesises
117-
var extractedLine = lines[0] # doesn't include command
118-
if extractedLine.endsWith(")"):
119-
# check if the ending ")" has a matching "(", otherwise remove it.
120-
var nOpen: int
121-
var i = startPos.column
122-
# count the number of opening brackets before code starts.
123-
while line[i-1] in Whitespace or line[i-1] == '(':
124-
if line[i-1] == '(':
125-
nOpen += 1
126-
i -= 1
127-
var nRemoved: int
128-
while nRemoved < nOpen: # remove last char until we have removed correct number of parentesis
129-
# We assume we are given correct Nim code and thus won't have to check what we remove, it should either be Whitespace or ')'
130-
assert extractedLine[^1] in Whitespace or extractedLine[^1] == ')', "Unexpected ending of string during parsing. Single line expression ended with character that wasn't whitespace of ')'."
131-
if extractedLine[^1] == ')':
132-
nRemoved += 1
133-
extractedLine.setLen(extractedLine.len-1)
134-
codeText = extractedLine
120+
let startsOnCommandLine = block:
121+
let preline = lines[0][0 ..< rawStartCol]
122+
startLine == rawStartLine and (not preline.isEmptyOrWhitespace) and (not (preline.nimIdentNormalize.strip() in ["for", "type"]))
123+
124+
if startsOnCommandLine:
125+
lines[0] = lines[0][rawStartCol .. ^1].strip()
126+
127+
if startLine == endLine and startsOnCommandLine:
128+
# single line expression
129+
var line = lines[0] # doesn't include command, but includes opening parenthesis
130+
while line.startsWith('(') and line.endsWith(')'):
131+
line = line[1 .. ^2].strip()
132+
133+
result = line
135134
else: # multi-line expression
135+
let baseIndent = skipWhile(rawLines[startLine], {' '})
136136
var preserveIndent: bool = false
137137
for i in 0 .. lines.high:
138138
let line = lines[i]
@@ -141,93 +141,23 @@ proc getCodeBlock*(source, command: string, startPos, endPos: Pos): string =
141141
lines[i] = line.substr(baseIndent)
142142
if nonMatching: # there is a non-matching triple-quote string
143143
preserveIndent = not preserveIndent
144-
codeText = lines.join("\n")
145-
result = codeText
146-
147-
148-
149-
150-
func getCodeBlockOld*(source: string, command: string, startPos, endPos: Pos): string =
151-
## Extracts the code in source from startPos to endPos with additional processing to get the entire code block.
152-
let lines = source.split("\n")
153-
var startLine = startPos.line - 1
154-
var endLine = endPos.line - 1
155-
debugecho "Start line: ", startLine + 1, startPos
156-
debugecho "End line: ", endLine + 1, endPos
157-
var codeText: string
158-
if not lines[startLine].isCommandLine(command): # multiline case
159-
while 0 < startLine and not lines[startLine-1].isCommandLine(command):
160-
#[ cases like this reports the third line instead of the second line:
161-
nbCode:
162-
let # this is the line we want
163-
x = 1 # but this is the one we get
164-
]#
165-
dec startLine
166-
167-
let indent = skipWhile(lines[startLine], {' '})
168-
let indentStr = " ".repeat(indent)
169-
170-
if lines[endLine].count("\"\"\"") == 1: # only opening of triple quoted string found. Rest is below it.
171-
inc endLine # bump it to not trigger the loop to immediately break
172-
while endLine < lines.high and "\"\"\"" notin lines[endLine]:
173-
inc endLine
174-
debugecho "Triple quote: ", lines[endLine]
175-
176-
while endLine < lines.high and (lines[endLine+1].startsWith(indentStr) or lines[endLine+1].isEmptyOrWhitespace):# and lines[endLine+1].strip().startsWith("#"):
177-
# Ending Comments should be included as well, but they won't be included in the AST -> endLine doesn't take them into account.
178-
# Block comments must be properly indented (including the content)
179-
inc endLine
180-
181-
var codeLines = lines[startLine .. endLine]
182-
183-
var notIndentLines: seq[int] # these lines are not to be adjusted for indentation. Eg content of triple quoted strings.
184-
var i: int
185-
while i < codeLines.len:
186-
if codeLines[i].count("\"\"\"") == 1:
187-
# We must do the identification of triple quoted string separatly from the endLine bumping because the triple strings
188-
# might not be the last expression in the code block.
189-
inc i # bump it to not trigger the loop to immediately break on the initial """
190-
notIndentLines.add i
191-
while i < codeLines.len and "\"\"\"" notin codeLines[i]:
192-
inc i
193-
notIndentLines.add i
194-
inc i
195-
196-
let parsedLines = collect(newSeqOfCap(codeLines.len)):
197-
for i in 0 .. codeLines.high:
198-
if i in notIndentLines:
199-
codeLines[i]
200-
else:
201-
codeLines[i].substr(indent)
202-
codeText = parsedLines.join("\n")
203-
elif lines[startLine].isCommandLine(command) and "\"\"\"" in lines[startLine]: # potentially multiline string
204-
discard
205-
else: # single line case, eg `nbCode: echo "Hello World"`
206-
let line = lines[startLine]
207-
var extractedLine = line[startPos.column .. ^1].strip()
208-
if extractedLine.strip().endsWith(")"):
209-
# check if the ending ")" has a matching "(", otherwise remove it.
210-
var nOpen: int
211-
var i = startPos.column
212-
# count the number of opening brackets before code starts.
213-
while line[i-1] in Whitespace or line[i-1] == '(':
214-
if line[i-1] == '(':
215-
nOpen += 1
216-
i -= 1
217-
var nRemoved: int
218-
while nRemoved < nOpen: # remove last char until we have removed correct number of parentesis
219-
# We assume we are given correct Nim code and thus won't have to check what we remove, it should either be Whitespace or ')'
220-
assert extractedLine[^1] in Whitespace or extractedLine[^1] == ')', "Unexpected ending of string during parsing. Single line expression ended with character that wasn't whitespace of ')'."
221-
if extractedLine[^1] == ')':
222-
nRemoved += 1
223-
extractedLine.setLen(extractedLine.len-1)
224-
codeText = extractedLine
225-
return codeText
144+
result = lines.join("\n")
226145

227146
macro getCodeAsInSource*(source: string, command: static string, body: untyped): string =
228147
## Returns string for the code in body from source.
229148
# substitute for `toStr` in blocks.nim
230149
let startPos = startPos(body)
150+
let filename = startPos.filename.newLit
231151
let endPos = finishPos(body)
152+
let endFilename = endPos.filename.newLit
153+
232154
result = quote do:
233-
getCodeBlock(`source`, `command`, `startPos`, `endPos`)
155+
if `filename` notin nb.sourceFiles:
156+
nb.sourceFiles[`filename`] = readFile(`filename`)
157+
158+
doAssert `endFilename` == `filename`, """
159+
Code from two different files were found in the same nbCode!
160+
If you want to mix code from different files in nbCode, use -d:nimibCodeFromAst instead.
161+
If you are not mixing code from different files, please open an issue on nimib's Github with a minimal reproducible example."""
162+
163+
getCodeBlock(nb.sourceFiles[`filename`], `command`, `startPos`, `endPos`)

src/nimib/types.nim

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type
2020
thisFile*: AbsoluteFile
2121
filename*: string
2222
source*: string
23+
sourceFiles*: Table[string, string]
2324
initDir*: AbsoluteDir
2425
options*: NbOptions
2526
cfg*: NbConfig

tests/tsources.nim

+57-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ suite "test sources":
77
# the replace stuff needed on windows where the lines read from file will have windows native new lines
88
test $currentTest:
99
actual = nbBlock.code
10-
echo &"===\n---actual:\n{actual.repr}\n---expected\n{expected.repr}\n---\n==="
1110
check actual.nbNormalize == expected.nbNormalize
11+
if actual.nbNormalize != expected.nbNormalize:
12+
echo &"===\n---actual:\n{actual.repr}\n---expected\n{expected.repr}\n---\n==="
1213
currentTest += 1
1314

1415
var currentTest: int
@@ -107,4 +108,59 @@ end"""
107108
expected = "echo y"
108109
check
109110

111+
nbCode:
112+
block:
113+
let
114+
b = 1
115+
116+
expected = "block:\n let\n b = 1"
117+
check
118+
119+
template notNbCode(body: untyped) =
120+
nbCode:
121+
body
122+
123+
notNbCode:
124+
echo y
125+
126+
expected = "echo y"
127+
check
128+
129+
template `&`(a,b: int) = discard
130+
131+
nbCode:
132+
1 &
133+
2
134+
135+
expected = "1 &\n 2"
136+
check
137+
138+
nbCode:
139+
nb.context["no_source"] = true
140+
141+
expected = "nb.context[\"no_source\"] = true"
142+
check
143+
144+
nbCode: discard
145+
expected = "discard"
146+
check
147+
148+
nbCode:
149+
for n in 0 .. 1:
150+
discard
151+
expected = "for n in 0 .. 1:\n discard"
152+
check
153+
154+
template nbCodeInTemplate =
155+
nbCode:
156+
nb.renderPlans["nbText"] = @["mdOutputToHtml"]
157+
158+
nbCodeInTemplate()
159+
expected = """nb.renderPlans["nbText"] = @["mdOutputToHtml"]"""
160+
check
161+
162+
nbCode:
163+
type A = object
164+
expected = "type A = object"
165+
check
110166

0 commit comments

Comments
 (0)