First commit for the rebranding of Ubakye as Grafoscopio. For previous history look at: http://smalltalkhub.com/#!/~Offray/Ubakye

This commit is contained in:
Offray Vladimir Luna Cárdenas 2014-11-01 16:23:42 +00:00 committed by SantiagoBragagnolo
parent 9fd3ac8d52
commit 3b3dcdacea
6 changed files with 871 additions and 0 deletions

3
.project Normal file
View File

@ -0,0 +1,3 @@
{
'srcDirectory' : 'src'
}

3
src/.properties Normal file
View File

@ -0,0 +1,3 @@
{
#format : #tonel
}

View File

@ -0,0 +1,430 @@
"
An UbakyeExplorer is a graphical interface for showing trees and outlines.
Instance Variables
browser: <Object>
browser
- xxxxx
"
Class {
#name : #GrafoscopioBrowser,
#superclass : #Object,
#instVars : [
'browser',
'explorer',
'mainTree',
'workingFile'
],
#classVars : [
'DefaultUbakyeBrowser'
],
#category : #'Grafoscopio-UI'
}
{ #category : #'as yet unclassified' }
GrafoscopioBrowser class >> open [
^ self new open
]
{ #category : #'as yet unclassified' }
GrafoscopioBrowser class >> openLandscape [
^ self new openLandscape
]
{ #category : #'graphical interface' }
GrafoscopioBrowser >> bodyOn: constructor [
"Shows the body of a selected node"
(constructor text)
title: 'Cuerpo | Body ';
format:[:eachNode |
(eachNode body) isNil
ifTrue: [ '' ]
ifFalse: [ eachNode body]].
]
{ #category : #'graphical interface' }
GrafoscopioBrowser >> buildBrowser [
"Main method for building the interface for trees. Is getting long. Needs refactoring"
browser := GLMTabulator new
title: 'Grafoscopio'.
browser
column: #tree span: 2;
column: [ :c |
c row: #nodeBody span: 2;
row: #nodeHeader ] span: 4.
browser
updateOn: GLMItemAdded from: #yourself;
updateOn: GLMItemRemoved from: #yourself.
(browser transmit)
to: #tree;
andShow: [:a | self treeOn: a].
"Creating a self updatable body pane"
(browser transmit)
to: #nodeBody;
from: #tree;
andShow: [ :a | self bodyOn: a].
(browser transmit )
from: #tree port: #selection;
from: #nodeBody port: #text;
when: [:node :text | text notNil];
to: #nodeBody port: #neverland;
transformed: [:node :text | node body: text asString].
(browser transmit)
from: #tree;
to: #nodeHeader;
andShow: [ :h | self headerOn: h ].
(browser transmit )
from: #tree port: #selection;
from: #nodeHeader port: #text;
when: [:node :text | text notNil];
to: #nodeHeader port: #neverland1;
transformed: [:node :text | node header: text asString]
]
{ #category : #'graphical interface' }
GrafoscopioBrowser >> buildBrowserLandscape [
browser := GLMTabulator new
title: 'Grafoscopio'.
browser
column: #tree span: 3;
column: #body span: 3;
column: #code span: 3.
browser
updateOn: GLMItemAdded from: #yourself;
updateOn: GLMItemRemoved from: #yourself.
(browser transmit)
to: #tree;
andShow: [ :a |
(a tree) "Layout"
title: mainTree header;
children: [ :eachNode |
(eachNode children) isNil
ifTrue: [ #() ]
ifFalse:[ eachNode children ] ];
format:[:eachNode |
(eachNode header) isNil
ifTrue: [ '' ]
ifFalse: [ eachNode header ]];
"Adding nodes"
act: [:treePresentation |
(treePresentation selection) isNotNil
ifTrue: [treePresentation selection addNodeAfter].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousZoomIn
entitled: 'Add node';
"Removing nodes"
act: [:treePresentation |
(treePresentation selection isNotNil)
ifTrue: [(treePresentation selection parent)
removeNode: treePresentation selection]
ifFalse: [treePresentation entity removeLast].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousZoomOut
entitled: 'Remove node';
"Inspecting nodes, for debugging purposes, DISABLE THIS ON PRODUCTION"
act: [:treePresentation | treePresentation selection inspect ]
icon: GLMUIThemeExtraIcons glamorousInspect
entitled: 'Inspect selection';
"Move nodes in the same hierarchy"
act: [:treePresentation |
(treePresentation selection isNotNil)
ifTrue: [treePresentation selection moveBefore].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousUp
entitled: 'Move node up';
act: [:treePresentation |
(treePresentation selection isNotNil)
ifTrue: [treePresentation selection moveAfter].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousDown
entitled: 'Move node down';
act: [:treePresentation |
(treePresentation selection isNotNil)
ifTrue: [treePresentation selection promote].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousLeft
entitled: 'Move node left';
act: [:treePresentation |
(treePresentation selection isNotNil)
ifTrue: [treePresentation selection demote].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousRight
entitled: 'Move node left'.
].
(browser transmit)
to: #body;
from: #tree;
andShow: [ :a |
(a text)
title: 'Original';
format:[:eachNode |
(eachNode body) isNil
ifTrue: [ '' ]
ifFalse: [ eachNode body ]]].
(browser transmit)
to: #code;
andShow: [ :a |
a smalltalkCode
title: 'Transmediado';
populate: #acceptedCode
icon: GLMUIThemeExtraIcons glamorousAccept
on: $s entitled: 'Accept' with: [:text | text text ]].
]
{ #category : #bibliography }
GrafoscopioBrowser >> customKeys [
"Replaces the default key in a BibTeX file for a custom key taken from the 'shorttitle' field.
Useful when autogenerated BibTeX has cryptic or non easy to memorize keys (for example in some
Zotero exports)"
| bibFile bibliography |
bibFile := ((workingFile parent) children
detect: [:each | each basename endsWith: 'bib' ] ifNone: [ ]).
bibFile ifNotNil:
[ bibliography := CZBibParser parse: (bibFile contents).
1 to: (bibliography size) do: [:index |
(((bibliography entries at: index) fields at: 2) key = 'shorttitle')
ifTrue: [
(bibliography entries at: index)
key: ((bibliography entries at: index) fields at: 2) value ]]].
]
{ #category : #persistence }
GrafoscopioBrowser >> exportAsMarkdown: aTree on: locator [
locator writeStreamDo: [:stream | stream nextPutAll: aTree asMarkdown]
]
{ #category : #persistence }
GrafoscopioBrowser >> exportAsSton: aTree on: locator [
locator nextPutAll: (STON toStringPretty: aTree children)
]
{ #category : #'graphical interface' }
GrafoscopioBrowser >> headerOn: constructor [
"Shows the body of a selected node"
(constructor text)
title: 'Cabecera | Header ';
format:[:eachNode |
(eachNode header) isNil
ifTrue: [ '' ]
ifFalse: [ eachNode header]].
]
{ #category : #persistence }
GrafoscopioBrowser >> open [
"Opens a new browser with a default tree and assigns a default draft file for storing it.
This is changed when the file is saved with the 'Save as' menu option"
| draftsLocation |
self buildBrowser.
mainTree := GrafoscopioNode new.
mainTree becomeDefaultTree.
draftsLocation := FileSystem disk workingDirectory / 'Grafoscopio/Drafts'.
draftsLocation ensureCreateDirectory.
workingFile := draftsLocation / 'draft.ston'.
browser openOn: mainTree children.
]
{ #category : #persistence }
GrafoscopioBrowser >> openDefault [
"Opens a new browser with a default tree"
self buildBrowser.
mainTree := GrafoscopioNode new.
mainTree becomeDefaultTree.
browser openOn: mainTree children.
]
{ #category : #persistence }
GrafoscopioBrowser >> openFromFile [
"Opens a tree from a file. Maybe this should be integrated with the 'open' method"
| fileStream currentChildren |
self buildBrowser.
fileStream := UITheme builder
fileOpen: 'Elija un archivo .ston'
extensions: #('ston').
fileStream isNil ifTrue: [ ^nil ].
workingFile := fileStream name asFileReference.
currentChildren := (STON fromStream: fileStream).
mainTree := GrafoscopioNode new
header: 'Bootstrapping the research object';
level: 0.
mainTree children: currentChildren.
browser openOn: mainTree children.
]
{ #category : #'as yet unclassified' }
GrafoscopioBrowser >> openLandscape [
"Opens a new browser with a default tree in landscape mode"
self buildBrowserLandscape.
mainTree := GrafoscopioNode new.
mainTree becomeDefaultTree.
browser openOn: mainTree children.
]
{ #category : #persistence }
GrafoscopioBrowser >> openLast [
"Opens a new browser with a fixed 'last' working tree. May be this should be changed"
| workingFile currentChildren| "workingChildren would sound too cruel!"
self buildBrowser.
workingFile := '/home/offray/Documentos/U/Libertadores/Grafoscopio/bootstrapping-objeto-investigacion.ston' asFileReference readStream.
currentChildren := (STON fromStream: workingFile).
mainTree := GrafoscopioNode new
header: 'Bootstrapping the research object';
level: 0.
mainTree children: currentChildren.
browser openOn: mainTree children.
]
{ #category : #persistence }
GrafoscopioBrowser >> openWorking [
"Opens a new browser with the last working tree"
self buildBrowser.
mainTree := '/home/offray/Documentos/U/Libertadores/Grafoscopio/bootstrapping-objeto-investigacion.ston' asFileReference readStream.
browser openOn: mainTree children.
]
{ #category : #persistence }
GrafoscopioBrowser >> saveToFile [
"Saves the current tree to a file"
| file writeStream |
file := UITheme builder
fileSave: 'Guardar archivo como...'
extensions: #('ston')
path: nil.
file ifNil: [ self inform: 'Exportación cancelada'. ^ self ].
writeStream := file writeStream.
workingFile := writeStream name asFileReference.
[ self exportAsSton: mainTree on: writeStream ]
ensure: [ writeStream ifNotNil: #close ]
]
{ #category : #persistence }
GrafoscopioBrowser >> saveWorkingTree [
"Saves the current tree to the user predefined file location used when he/she opened it."
| fileStream markdownFile |
fileStream := workingFile writeStream.
markdownFile := (workingFile parent) / (workingFile basenameWithoutExtension, '.markdown').
[ self exportAsSton: mainTree on: fileStream.
self exportAsMarkdown: mainTree on: markdownFile ]
ensure: [ fileStream ifNotNil: #close.].
self customKeys.
self inform: 'Archivo Guardado'.
]
{ #category : #'graphical interface' }
GrafoscopioBrowser >> treeOn: constructor [
"Shows the correspondent tree of a node"
(constructor tree) "Layout"
title: mainTree header;
children: [ :eachNode |
(eachNode children) isNil
ifTrue: [ #() ]
ifFalse:[ eachNode children ] ];
format:[:eachNode |
(eachNode header) isNil
ifTrue: [ '' ]
ifFalse: [ eachNode header ]];
"Adding nodes"
act: [:treePresentation |
(treePresentation selection) isNotNil
ifTrue: [treePresentation selection addNodeAfter].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousZoomIn
entitled: 'Add node';
"Removing nodes"
act: [:treePresentation |
(treePresentation selection isNotNil)
ifTrue: [(treePresentation selection parent)
removeNode: treePresentation selection]
ifFalse: [treePresentation entity removeLast].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousZoomOut
entitled: 'Remove node';
"Move nodes in the same hierarchy"
act: [:treePresentation |
(treePresentation selection isNotNil)
ifTrue: [treePresentation selection moveBefore].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousUp
entitled: 'Move node up';
act: [:treePresentation |
(treePresentation selection isNotNil)
ifTrue: [treePresentation selection moveAfter].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousDown
entitled: 'Move node down';
act: [:treePresentation |
(treePresentation selection isNotNil)
ifTrue: [treePresentation selection promote].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousLeft
entitled: 'Move node left';
act: [:treePresentation |
(treePresentation selection isNotNil)
ifTrue: [treePresentation selection demote].
treePresentation update]
icon: GLMUIThemeExtraIcons glamorousRight
entitled: 'Move node rigt';
act: [self saveWorkingTree]
icon: GLMUIThemeExtraIcons glamorousSave
entitled: 'Save current tree';
"Menu options"
act: [ GrafoscopioBrowser new openFromFile] entitled: 'Abrir/Cargar ...';
act: [self saveToFile] entitled: 'Guardar como ...';
act: [self updateSystem] entitled: 'Actualizar Grafoscopio';
act: [:x | x printString inspect] entitled: 'Acerca de...'.
]
{ #category : #'as yet unclassified' }
GrafoscopioBrowser >> updateSystem [
"Updates the system with new versions of itself take from the source code repository"
Gofer new
smalltalkhubUser: 'SvenVanCaekenberghe' project: 'STON';
package: 'STON-Core';
load.
Gofer new squeaksource: 'Citezen';
package: 'ConfigurationOfCitezen';
load.
(ConfigurationOfCitezen project latestVersion: #development) load.
Gofer new
smalltalkhubUser: 'Offray' project: 'Ubakye';
package: 'Ubakye';
load.
]

View File

@ -0,0 +1,418 @@
"
An UbakyeNode is and administrator of all node operations in a tree.
Instance Variables
node: <Object>
node
- xxxxx
"
Class {
#name : #GrafoscopioNode,
#superclass : #Object,
#instVars : [
'header',
'headers',
'key',
'icon',
'body',
'children',
'parent',
'node',
'level',
'nodesInPreorder'
],
#category : #'Grafoscopio-Model'
}
{ #category : #'instance creation' }
GrafoscopioNode class >> header: aHeader body: aText [
"Create a new instance with given header and body"
^(self new)
header: aHeader;
body: aText;
yourself.
]
{ #category : #'instance creation' }
GrafoscopioNode class >> header: aHeader icon: anIcon body: aText [
"Create a new instances with given header, icon and body"
^(self new)
header: aHeader;
icon: anIcon;
body: aText;
yourself.
]
{ #category : #'instance creation' }
GrafoscopioNode class >> named: aString [
"Create a new instance with a given header and empty body"
^(self new)
header: aString;
yourself
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> addNode: aNode [
"Adds the given node to the receivers collection of children, and sets this object as the parent
of the node"
self children add: aNode.
aNode level: (self level) + 1.
aNode parent: self.
^aNode
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> addNodeAfter [
"Adds a generic node after the given node so they become slibings of the same parent"
| genericNode |
genericNode := GrafoscopioNode
header: 'nuevoNodo' body: ''.
self parent children add: genericNode after: self.
genericNode parent: self parent.
genericNode level: self level.
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> asMarkdown [
"Exports children of the curren node as pandoc markdown, using special nodes for config options and
bibliography."
| markdownOutput |
markdownOutput := '' writeStream.
(self preorderTraversal) do: [ :eachNode | markdownOutput nextPutAll: (eachNode markdownContent) ].
^markdownOutput contents
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> asSton [
"Exports current tree as STON format"
| stonOutput |
stonOutput := '' writeStream.
stonOutput nextPutAll: (STON toStringPretty: self children).
^stonOutput contents
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> becomeDefaultTree [
| node1 node2 node3 |
self level: 0.
self header: 'Arbol principal'.
node1 := GrafoscopioNode
header: 'Nodo 1'
body: 'Texto 1'.
node2 := GrafoscopioNode
header: 'Nodo 2'
body: 'Texto 2'.
node3 := GrafoscopioNode
header: 'Nodo 3'
body: 'Texto 3'.
self
addNode: node1;
addNode: node2.
node2 addNode: node3.
]
{ #category : #accessing }
GrafoscopioNode >> body [
"Returns the receivers body"
^ body
]
{ #category : #accessing }
GrafoscopioNode >> body: anObject [
"Sets the receivers body to the given object"
body := anObject
]
{ #category : #accessing }
GrafoscopioNode >> children [
"Returns the receivers list of children"
^ children ifNil: [children := OrderedCollection new]
]
{ #category : #accessing }
GrafoscopioNode >> children: aCollection [
"Sets the receivers children"
aCollection do: [:currentNode | currentNode parent: self ].
children := aCollection.
]
{ #category : #movement }
GrafoscopioNode >> demote [
"Moves the current node down in the hierachy, making a children of its current previous slibing"
| collection index predecessor |
collection := self parent children.
index := collection indexOf: self.
(index between: 2 and: collection size)
ifTrue: [ predecessor := collection before: self.
collection remove: self.
predecessor addNode: self]
]
{ #category : #accessing }
GrafoscopioNode >> header [
"Returns the receiver header"
^ header
]
{ #category : #accessing }
GrafoscopioNode >> header: anObject [
"Sets the receivers header"
header := anObject
]
{ #category : #accessing }
GrafoscopioNode >> headers [
"Returns the headers of the receiver children"
^ headers := self children collect: [:currentNode | currentNode header ]
]
{ #category : #accessing }
GrafoscopioNode >> icon [
"Returns the receivers icon"
^icon
]
{ #category : #accessing }
GrafoscopioNode >> icon: aSymbol [
"Sets the receivers icon"
icon := aSymbol
]
{ #category : #initialization }
GrafoscopioNode >> initialize [
"Creates a empty new node"
super initialize.
self header: 'newHeader'.
self body: ''.
self key: ''
]
{ #category : #accessing }
GrafoscopioNode >> key [
"Returns a unique key identifying the receiver in the help system"
^key
]
{ #category : #accessing }
GrafoscopioNode >> key: aUniqueKey [
"Sets a unique key identifying the receiver in the help system"
key := aUniqueKey
]
{ #category : #accessing }
GrafoscopioNode >> level [
"Returns the level of the node. See the setter message for details"
^level
]
{ #category : #accessing }
GrafoscopioNode >> level: anInteger [
"Sets the node level in a hierarchy. The only node with level 0 is the root node and from there levels increase
in 1 for its direct children, 2 for its grand children and so on. Silibings nodes has the same level"
level := anInteger
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> markdownContent [
"Extracts the markdown of a node using body as content, header as title and level as hierarchical level of the title.
If special nodes types are present, converts them into proper markup to be embedded inside markdown"
| markdown configDict specialWords embedNodes |
markdown := '' writeStream.
specialWords := #('#config' '#abstract' '#invisible' '#idea' '#footnote' 'nuevoNodo' '%embed').
(self level > 0) & (specialWords includes: self header) not & (specialWords includes: ((self header findTokens: $ ) at: 1)) not
ifTrue: [
self level timesRepeat: [ markdown nextPutAll: '#' ].
markdown nextPutAll: ' '.
markdown nextPutAll: (self header copyReplaceTokens: #cr with: #lf); lf; lf.
embedNodes := self headers select: [:each | ((each findTokens: $ ) at: 1) = '%embed'].
markdown nextPutAll: (self body contents withUnixLineEndings); lf; lf].
(self header = '#config')
ifTrue: [
configDict := STON fromString: (self body).
markdown nextPutAll: '---'; lf.
markdown nextPutAll: 'title: ', (configDict at: 'title'); lf.
markdown nextPutAll: 'author: ', ((configDict at: 'author') at: 'given'), ' ', ((configDict at: 'author') at: 'family'); lf.
markdown nextPutAll: 'bibliography: ', (configDict at: 'bibliography'); lf.
markdown nextPutAll: 'abstract: ', '|'; lf; nextPutAll: (configDict at: 'abstract'); lf.
markdown nextPutAll: '---'; lf. ].
((self header findString: '#idea') = 1)
ifTrue: [markdown nextPutAll: (self body contents withInternetLineEndings); lf; lf. ].
((self header findString: '#footnote') = 1)
ifTrue: [
markdown nextPutAll: ('[^',(self header copyReplaceAll: '#footnote ' with: ''),']: ' ); lf.
markdown nextPutAll: (self body contents withInternetLineEndings); lf; lf. ].
((self header findString: '%embed') = 1)
ifTrue: "[markdown nextPutAll: (self body contents withInternetLineEndings); lf; lf. ]."
[markdown nextPutAll: (((self parent body) copyReplaceAll: (self header) with: (self body)) contents withInternetLineEndings); lf; lf. ].
^markdown contents
]
{ #category : #movement }
GrafoscopioNode >> moveAfter [
"Moves the current node a place before in the children collection where is located"
| collection index successor |
collection := self parent children.
index := collection indexOf: self.
(index between: 1 and: collection size - 1)
ifTrue: [
successor := collection after: self.
collection at: index + 1 put: self.
collection at: index put: successor]
]
{ #category : #movement }
GrafoscopioNode >> moveBefore [
"Moves the current node a place before in the children collection where is located"
| collection index predecessor |
collection := self parent children.
index := collection indexOf: self.
(index between: 2 and: collection size)
ifTrue: [
predecessor := collection before: self.
collection at: index -1 put: self.
collection at: index put: predecessor]
]
{ #category : #'instance creation' }
GrafoscopioNode >> newNode [
node := Dictionary newFrom: {
#header -> 'newHeadline'.
#body -> ''.
#children -> #()}.
^ node.
]
{ #category : #accessing }
GrafoscopioNode >> parent [
"Returns the parent of the current node"
^ parent
]
{ #category : #accessing }
GrafoscopioNode >> parent: aNode [
"A parent is a node that has the current node in its children"
parent := aNode
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> preorderTraversal [
nodesInPreorder := OrderedCollection new.
self visitedGoTo: nodesInPreorder.
^ nodesInPreorder.
]
{ #category : #movement }
GrafoscopioNode >> promote [
"Moves the current node up in the hierachy, making it a slibing of its current parent"
| collection grandparent |
collection := self parent children.
grandparent := self parent parent.
collection isNotNil & grandparent isNotNil
ifTrue: [
(grandparent children) add: self after: (self parent).
self level: (self parent) level.
self parent: grandparent.
collection remove: self.]
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> removeLastNode [
"Adds the given node to the receivers collection of children, and sets this object as the parent
of the node"
self children removeLast.
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> removeNode: aNode [
"Adds the given node to the receivers collection of children, and sets this object as the parent
of the node"
self children remove: aNode.
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> returnConfig [
"Detects a children node containing the configuration for the creation of the output files. If nothing is detected,
creates defaults for that. Pending:
- Verifying that file data in config node is accurate, and if not create the proper locations.
- Maybe there is a need to consider if it should run on all the tree, instead of a particular node."
| configString configNode configDict |
configNode := (self children) detect: [:nodeContent | nodeContent header = 'Config' ] ifNone: [ nil ].
"This part always enter for the nil option! MUST BE CORRECTED"
configNode isNil
ifTrue: [
configString := '{
"title": "Boostrapping para el objeto de investigación",
"author": {
"given": "Offray Vladimir",
"family": "Luna Cárdenas"
},
"file": {
"relative-path" : "U/Libertadores/Grafoscopio/",
"name": "bootstrapping-objeto-investigacion",
"formats": [
#ston,
#markdown
]
}
}'
]
ifFalse: [ configString := configNode body ].
configDict := STON fromString: configString.
^configDict
]
{ #category : #'as yet unclassified' }
GrafoscopioNode >> visitedGoTo: aCollection [
"Stores the current node in a collection and recursively stores its children"
aCollection add: self.
(self children isNotEmpty) & ((self header findString: '#invisible')=1) not
ifTrue: [ (self children) do: [ :eachNode | eachNode visitedGoTo: aCollection]].
]

View File

@ -0,0 +1,16 @@
"
An UbakyeBook is a booklet to compile all documentation needed
in the making of the Ubakye outliner and visual data narrative
environment.
"
Class {
#name : #UbakyeBook,
#superclass : #CustomHelp,
#category : #'Grafoscopio-Help'
}
{ #category : #'as yet unclassified' }
UbakyeBook >> bookName [
^'Visual Data Narratives with Pharo/Smalltalk'
]

View File

@ -0,0 +1 @@
Package { #name : #Grafoscopio }