diff --git a/src/Microdown-BookTester-Tests/MiCheckerEngineTest.class.st b/src/Microdown-BookTester-Tests/MiCheckerEngineTest.class.st deleted file mode 100644 index 776658e7..00000000 --- a/src/Microdown-BookTester-Tests/MiCheckerEngineTest.class.st +++ /dev/null @@ -1,99 +0,0 @@ -Class { - #name : 'MiCheckerEngineTest', - #superclass : 'TestCase', - #instVars : [ - 'fileSystem', - 'engine', - 'file' - ], - #category : 'Microdown-BookTester-Tests', - #package : 'Microdown-BookTester-Tests' -} - -{ #category : 'running' } -MiCheckerEngineTest >> generateFilesystemExample [ - file := fileSystem workingDirectory / 'test.md'. - file writeStreamDo: [ :stream | - stream nextPutAll: 'The colour of the sky is blue. -## writing books in pharo' - ]. -] - -{ #category : 'running' } -MiCheckerEngineTest >> setUp [ - super setUp. - - fileSystem := FileSystem memory. - engine := MicCheckerEngine new. - self generateFilesystemExample. -] - -{ #category : 'running' } -MiCheckerEngineTest >> testCheckersFromConfiguration [ - | config checkers | - config := OrderedDictionary new. - config at: 'EnglishTypography' put: nil. - config at: 'HeaderCapitalization' put: 'lowercase'. - checkers := engine checkersFromConfiguration: config. - self assert: checkers size equals: 2. - self assert: (checkers anySatisfy: [:c | c isKindOf: MicEnglishTypographyChecker]). - self assert: (checkers anySatisfy: [:c | c isKindOf: MicRuleHeaderChecker]). -] - -{ #category : 'running' } -MiCheckerEngineTest >> testCheckersFromConfigurationAllCheckers [ - | config checkers | - config := OrderedDictionary new. - config at: 'EnglishTypography' put: nil. - config at: 'FrenchTypography' put: nil. - config at: 'HeaderCapitalization' put: 'lowercase'. - config at: 'Vocabulary' put: { #('colour' 'color')}. - config at: 'CodeIndentation' put: nil. - config at: 'MethodReferenceUsingHash' put: nil. - config at: 'MethodStructure' put: nil. - config at: 'NotTheFigureX' put: nil. - config at: 'CodeLastPeriod' put: 'true'. - config at: 'ParagraphLabelEndPeriod' put: 'true'. - config at: 'CodeLastPeriodInCaption' put: nil. - config at: 'LastPeriodInCode' put: nil. - checkers := engine checkersFromConfiguration: config. - self assert: checkers size equals: 12. -] - -{ #category : 'running' } -MiCheckerEngineTest >> testEngineDetectsHeaderMistake [ - | config results | - config := OrderedDictionary new. - config at: 'HeaderCapitalization' put: 'uppercase'. - results := engine runOn: file withConfiguration: config. - self deny: results isEmpty. - self assert: results size equals: 3. -] - -{ #category : 'running' } -MiCheckerEngineTest >> testEngineRunsMultipleCheckers [ - | config results | - config := OrderedDictionary new. - config at: 'Vocabulary' put: { #('colour' 'color') }. - config at: 'HeaderCapitalization' put: 'uppercase'. - results := engine runOn: file withConfiguration: config. - self deny: results isEmpty. - self assert: results size > 1. -] - -{ #category : 'running' } -MiCheckerEngineTest >> testEngineWithEmptyConfiguration [ - | config results | - config := OrderedDictionary new. - results := engine runOn: file withConfiguration: config. - self assert: results isEmpty. -] - -{ #category : 'running' } -MiCheckerEngineTest >> testEngineWithUnknownChecker [ - | config results | - config := OrderedDictionary new. - config at: 'UnknownChecker' put: nil. - results := engine runOn: file withConfiguration: config. - self assert: results isEmpty. -] diff --git a/src/Microdown-BookTester/MicAnalysisReportWriter.class.st b/src/Microdown-BookTester/MicAnalysisReportWriter.class.st index ff64d733..cfb816ab 100644 --- a/src/Microdown-BookTester/MicAnalysisReportWriter.class.st +++ b/src/Microdown-BookTester/MicAnalysisReportWriter.class.st @@ -19,7 +19,7 @@ MicAnalysisReportWriter class >> checkersFromConfiguration: aConfiguration [ | availableCheckers configuredCheckers onlyCheckers | availableCheckers := Dictionary new. - (MicChecker allSubclasses copyWithoutFirst: MicFrenchTypoChecker) + MicChecker allSubclasses do: [ :aClass | availableCheckers at: aClass checkerName diff --git a/src/Microdown-BookTester/MicCheckerDashboard.class.st b/src/Microdown-BookTester/MicCheckerDashboard.class.st new file mode 100644 index 00000000..2efcf09b --- /dev/null +++ b/src/Microdown-BookTester/MicCheckerDashboard.class.st @@ -0,0 +1,172 @@ +Class { + #name : 'MicCheckerDashboard', + #superclass : 'SpPresenter', + #instVars : [ + 'checkerList', + 'explanation', + 'filePathInput', + 'runButton', + 'results', + 'browseButton', + 'exportButton', + 'reporter', + 'file', + 'currentFile', + 'currentReporter', + 'fileContent' + ], + #category : 'Microdown-BookTester-CheckerDashboard', + #package : 'Microdown-BookTester', + #tag : 'CheckerDashboard' +} + +{ #category : 'initialization' } +MicCheckerDashboard >> connectPresenters [ + checkerList presenters do: [ :cb | + cb whenChangedDo: [ + cb state ifTrue: [ + | name checker | + name := cb label. + checker := MicChecker allSubclasses + detect: [ :c | c checkerName = name ] + ifNone: [ nil ]. + checker ifNotNil: [ + explanation text: checker explanationForConfiguration. + name = 'FrenchTypography' ifTrue: [ + checkerList presenters + detect: [ :c | c label = 'EnglishTypography' ] + ifFound: [ :c | c state: false ] ]. + name = 'EnglishTypography' ifTrue: [ + checkerList presenters + detect: [ :c | c label = 'FrenchTypography' ] + ifFound: [ :c | c state: false ] ] ] ] ] ]. + + runButton action: [ + | config output selectedCheckers | + filePathInput text isEmpty + ifTrue: [ results text: 'Please enter a file path.' ] + ifFalse: [ + currentFile := filePathInput text asFileReference. + currentFile exists + ifFalse: [ results text: 'File not found: ' , currentFile fullName ] + ifTrue: [ + selectedCheckers := (checkerList presenters + select: [ :cb | cb state ]) + collect: [ :cb | cb label ]. + selectedCheckers isEmpty + ifTrue: [ results text: 'Please select at least one checker.' ] + ifFalse: [ + results text: 'Checking... please wait.'. + config := ConfigurationForPillar newFromDictionary: + (Dictionary new + at: 'onlyCheckers' put: selectedCheckers; + yourself). + currentReporter := MicAnalysisReportWriter + reportForFolder: currentFile parent + startingFrom: currentFile + configuration: config. + currentReporter isOkay + ifTrue: [ results text: 'No problems found!' ] + ifFalse: [ + | numberedContent lines | + output := String streamContents: [ :str | + currentReporter buildReportOn: str ]. + results text: output. + exportButton enabled: true. + + lines := currentFile contents lines. + numberedContent := String streamContents: [ :s | + lines doWithIndex: [ :line :i | + s nextPutAll: i printString. + s nextPutAll: ' | '. + s nextPutAll: line. + s nextPut: Character lf ] ]. + fileContent text: numberedContent ] ] ] ] ]. + + browseButton action: [ + | chosen | + chosen := UIManager default + chooseExistingFileReference: 'Select index.md' + extensions: #('md') + path: FileSystem workingDirectory. + chosen ifNotNil: [ filePathInput text: chosen fullName ] ]. + + exportButton action: [ + | htmlReporter outputFile | + htmlReporter := MicCheckerHTMLReporter new. + htmlReporter reporter: currentReporter. + htmlReporter fileReference: currentFile. + outputFile := htmlReporter generateReport. + WebBrowser openOn: outputFile fullName. + results text: results text , String cr , 'HTML report exported to: ' , outputFile fullName ]. +] + +{ #category : 'initialization' } +MicCheckerDashboard >> defaultLayout [ + | notebook topSection | + notebook := self newNotebook. + notebook addPage: (self newNotebookPage + title: 'Results'; + presenterProvider: [ results ]; + yourself). + notebook addPage: (self newNotebookPage + title: 'File Content'; + presenterProvider: [ fileContent ]; + yourself). + topSection := SpBoxLayout newTopToBottom + add: (SpPanedLayout newLeftToRight + add: checkerList; + add: explanation; + yourself); + addLast: (SpBoxLayout newLeftToRight + add: filePathInput; + add: browseButton expand: false; + yourself) expand: false; + addLast: (SpBoxLayout newLeftToRight + add: runButton; + add: exportButton; + yourself) expand: false; + yourself. + ^ SpPanedLayout newTopToBottom + add: topSection; + add: notebook; + yourself +] + +{ #category : 'initialization' } +MicCheckerDashboard >> initializePresenters [ + checkerList := self newComponentList. + checkerList presenters: (MicChecker allSubclasses collect: [ :c | + | cb | + cb := self newCheckBox. + cb label: c checkerName. + cb ]). + explanation := self newText. + explanation text: 'Select a checker to see its explanation.'. + filePathInput := self newTextInput. + filePathInput placeholder: 'Path to index.md...'. + results := self newText. + results text: 'Results will appear here.'. + results beNotEditable. + fileContent := self newText. + fileContent beNotEditable. + runButton := self newButton. + runButton label: 'Run Checkers'. + exportButton := self newButton. + exportButton label: 'Export HTML Report'. + exportButton enabled: false. + browseButton := self newButton. + browseButton label: 'Browse'. +] + +{ #category : 'initialization' } +MicCheckerDashboard >> initializeWindow: aWindowPresenter [ + aWindowPresenter title: 'Microdown Checker Dashboard'; + initialExtent: 900 @ 700. +] + +{ #category : 'accessing' } +MicCheckerDashboard >> runButton [ + + ^ runButton +] diff --git a/src/Microdown-BookTester/MicCheckerEngine.class.st b/src/Microdown-BookTester/MicCheckerEngine.class.st deleted file mode 100644 index fd770e3a..00000000 --- a/src/Microdown-BookTester/MicCheckerEngine.class.st +++ /dev/null @@ -1,52 +0,0 @@ -" -I am a generic engine that reads a configuration dictionary and instantiates, configures and runs the appropriate checkers. -I discover all available checkers by looking at all subclasses of MicChecker that respond to #checkerName. - -Usage example: - | engine config reslts | - config := OrderedDictionary new. - config at: 'Vocabulary' put: { ('colour' -> 'color') }. - config at: 'HeaderCapitalization' put: 'uppercase'. - config at: 'EnglishTypographic' put:nil. - engine := MicCheckerEngine new. - results := engine runOn: 'myFile.md' asFileReference withConfiguration: config. - results do: [ :r | Transcript showCr: r explanation ]. -" -Class { - #name : 'MicCheckerEngine', - #superclass : 'Object', - #category : 'Microdown-BookTester-Core', - #package : 'Microdown-BookTester', - #tag : 'Core' -} - -{ #category : 'as yet unclassified' } -MicCheckerEngine >> checkersFromConfiguration: aConfiguration [ - | availableCheckers activeCheckers | - availableCheckers := Dictionary new. - MicChecker allSubclasses do: [ :aClass | - availableCheckers at: aClass checkerName put: aClass ]. - activeCheckers := OrderedCollection new. - aConfiguration keysAndValuesDo: [ :key :value | - | checkerClass checker | - checkerClass := availableCheckers at: key ifAbsent: [ nil ]. - checkerClass ifNotNil: [ - checker := checkerClass new. - checker configureFrom: value. - activeCheckers add: checker - ] - ]. - ^ activeCheckers -] - -{ #category : 'as yet unclassified' } -MicCheckerEngine >> runOn: aFileReference withConfiguration: aConfiguration [ - | activeCheckers results | - activeCheckers := self checkersFromConfiguration: aConfiguration. - results := OrderedCollection new. - activeCheckers do: [ :checker | - checker checkProject: aFileReference. - results addAll: checker results - ]. - ^ results -] diff --git a/src/Microdown-BookTester/MicCheckerHTMLReporter.class.st b/src/Microdown-BookTester/MicCheckerHTMLReporter.class.st new file mode 100644 index 00000000..e8b878cd --- /dev/null +++ b/src/Microdown-BookTester/MicCheckerHTMLReporter.class.st @@ -0,0 +1,116 @@ +Class { + #name : 'MicCheckerHTMLReporter', + #superclass : 'Object', + #instVars : [ + 'reporter', + 'fileReference' + ], + #category : 'Microdown-BookTester-CheckerDashboard', + #package : 'Microdown-BookTester', + #tag : 'CheckerDashboard' +} + +{ #category : 'accessing' } +MicCheckerHTMLReporter >> buildHTML [ + ^ String streamContents: [ :s | + s nextPutAll: ' + +Checker Report +'. + s nextPutAll: '

Microdown Checker Report

'. + s nextPutAll: '
File: ' , fileReference fullName , '
'. + s nextPutAll: 'Total errors: ' , reporter results size printString. + s nextPutAll: '
'. + reporter isOkay + ifTrue: [ s nextPutAll: '

✓ No problems found!

' ] + ifFalse: [ self buildResultsOn: s]. + s nextPutAll: '' + ] +] + +{ #category : 'accessing' } +MicCheckerHTMLReporter >> buildResultsOn: aStream [ + | grouped | + grouped := reporter results groupedBy: [:r | r headerString]. + grouped keysAndValuesDo: [:header :results | + aStream nextPutAll: '

' , header , '

'. + results do: [:r | + | lineNum srcFile searchText | + srcFile := r sourceFileReference ifNil: [ fileReference ]. + searchText := (r micElement isNil or: [r micElement bodyString isNil]) + ifTrue: [ r detail ifNil: [''] ] + ifFalse: [ r micElement bodyString ]. + lineNum := (srcFile notNil and: [searchText notEmpty]) + ifTrue: [ self lineNumberOf: searchText inFile: srcFile ] + ifFalse: [ 0 ]. + aStream nextPutAll: '
'. + lineNum > 0 ifTrue: [ + aStream nextPutAll: 'Line ' , lineNum printString , ': ' ]. + aStream nextPutAll: r explanation. + lineNum > 0 ifTrue: [ + | context | + context := self contextAround: lineNum inFile: srcFile. + aStream nextPutAll: '
' , context , '
' ]. + aStream nextPutAll: '
' ] ] +] + +{ #category : 'accessing' } +MicCheckerHTMLReporter >> contextAround: lineNum inFile: aFileRef [ + | lines start end | + lines := aFileRef contents lines. + start := (lineNum - 2) max: 1. + end := (lineNum + 2) min: lines size. + ^ String streamContents: [:s | + start to: end do: [:i | + i = lineNum + ifTrue: [ s nextPutAll: '' ] + ifFalse: [ s nextPutAll: '' ]. + s nextPutAll: i printString , ' | ' , (lines at: i). + s nextPutAll: ''. + s nextPut: Character lf]] +] + +{ #category : 'accessing' } +MicCheckerHTMLReporter >> fileReference: aFileReference [ + + fileReference := aFileReference +] + +{ #category : 'accessing' } +MicCheckerHTMLReporter >> generateReport [ + | html outputFile | + html := self buildHTML. + outputFile := fileReference parent / '_result' / 'checker-report.html'. + outputFile parent ensureCreateDirectory. + outputFile writeStreamDo: [ :s | s nextPutAll: html ]. + ^ outputFile + +] + +{ #category : 'accessing' } +MicCheckerHTMLReporter >> lineNumberOf: aString inFile: aFileRef [ + | lines | + (aString isNil or: [aString isEmpty ]) ifTrue: [ ^ 0 ]. + aFileRef exists ifFalse: [ ^ 0 ]. + lines := aFileRef readStream contents lines. + ^ lines detectIndex: [ :line | + line includesSubstring: aString ] + ifNone: [ 0 ] + +] + +{ #category : 'accessing' } +MicCheckerHTMLReporter >> reporter: aReporter [ + + reporter := aReporter +] diff --git a/src/Microdown-BookTester/MicResult.class.st b/src/Microdown-BookTester/MicResult.class.st index b982b8bf..d9dda607 100644 --- a/src/Microdown-BookTester/MicResult.class.st +++ b/src/Microdown-BookTester/MicResult.class.st @@ -41,6 +41,11 @@ MicResult >> explanation [ ^ self subclassResponsibility ] +{ #category : 'kinds' } +MicResult >> headerString [ + ^ self class name +] + { #category : 'accessing' } MicResult >> message [ diff --git a/src/Microdown-Rules/MicAbstractResult.class.st b/src/Microdown-Rules/MicAbstractResult.class.st index bad48b0a..065795e4 100644 --- a/src/Microdown-Rules/MicAbstractResult.class.st +++ b/src/Microdown-Rules/MicAbstractResult.class.st @@ -37,3 +37,8 @@ MicAbstractResult >> micElement [ MicAbstractResult >> micElement: aMicElement [ micElement := aMicElement ] + +{ #category : 'accessing' } +MicAbstractResult >> sourceFileReference [ + ^ fileReference +] diff --git a/src/Microdown-Rules/MicEnglishTypographyChecker.class.st b/src/Microdown-Rules/MicEnglishTypographyChecker.class.st index 50861840..1e0b053c 100644 --- a/src/Microdown-Rules/MicEnglishTypographyChecker.class.st +++ b/src/Microdown-Rules/MicEnglishTypographyChecker.class.st @@ -20,6 +20,15 @@ MicEnglishTypographyChecker class >> checkerName [ ^ 'EnglishTypography' ] +{ #category : 'configuration help' } +MicEnglishTypographyChecker class >> explanationForConfiguration [ + ^ 'I check that punctuation follows English typography rules. No space before : ; ? ! and space after : , ; ? ! + URLs (http:// and https://) are ignored. + No configuration needed. + In pillar.conf add: + "onlyCheckers" : [ "EnglishTypography" ]' +] + { #category : 'adding' } MicEnglishTypographyChecker >> addResultFor: anElement message: aMessage [ results add: (MicEnglishTypographyResult new diff --git a/src/Microdown-Rules/MicEnglishTypographyResult.class.st b/src/Microdown-Rules/MicEnglishTypographyResult.class.st index 7f306f47..3de6180a 100644 --- a/src/Microdown-Rules/MicEnglishTypographyResult.class.st +++ b/src/Microdown-Rules/MicEnglishTypographyResult.class.st @@ -15,6 +15,12 @@ MicEnglishTypographyResult class >> headerString [ ^ 'English Typography Violations' ] +{ #category : 'accessing' } +MicEnglishTypographyResult >> detail [ + + ^ detail +] + { #category : 'accessing' } MicEnglishTypographyResult >> detail: aString [ detail := aString @@ -30,3 +36,8 @@ MicEnglishTypographyResult >> explanation [ ^ 'Text: "' , text , '" in file' , fileReference fullName , ' contains a mistake: ', detail ] + +{ #category : 'kinds' } +MicEnglishTypographyResult >> headerString [ + ^ 'English Typography violations' +] diff --git a/src/Microdown-Rules/MicLastPeriodChecker.class.st b/src/Microdown-Rules/MicLastPeriodChecker.class.st index 194ff29a..af5cd32f 100644 --- a/src/Microdown-Rules/MicLastPeriodChecker.class.st +++ b/src/Microdown-Rules/MicLastPeriodChecker.class.st @@ -1,3 +1,15 @@ +" +I check that code blocks end with a period or not, depending on the configuration. + +I can be configured in two modes: +- identifyMissingPeriod (true): flags code blocks that do NOT end with a period. +- identifyExtraPeriod (false): flags code blocks that DO end with a period. + +In pillar.conf add: + ""checkerConfigurations"" : [ + ""CodeLastPeriod"" : ""true"" + ] +" Class { #name : 'MicLastPeriodChecker', #superclass : 'MicChecker', diff --git a/src/Microdown-Rules/MicRuleHeaderChecker.class.st b/src/Microdown-Rules/MicRuleHeaderChecker.class.st index ee4a4470..ba796196 100644 --- a/src/Microdown-Rules/MicRuleHeaderChecker.class.st +++ b/src/Microdown-Rules/MicRuleHeaderChecker.class.st @@ -1,3 +1,13 @@ +" +I check that headers follow a consistent capitalization strategy (uppercase or lowercase). + +The first word of a header is always capitalized. +Common words like 'a', 'an', 'the', 'and', etc. are not capitalized. + +I can be configured in two modes: +- uppercase: all significiant words should be capitalized +- lowercase: only the first word should be capitalized +" Class { #name : 'MicRuleHeaderChecker', #superclass : 'MicChecker',