674 lines
16 KiB
Smalltalk
674 lines
16 KiB
Smalltalk
"
|
|
I model a Tiddler object in [TiddlyWiki](https://tiddlywiki.com/).
|
|
|
|
I implement the standard fields as described in the standard documentation at: <https://tiddlywiki.com/#TiddlerFields>
|
|
|
|
"
|
|
Class {
|
|
#name : #Tiddler,
|
|
#superclass : #Object,
|
|
#instVars : [
|
|
'title',
|
|
'text',
|
|
'modified',
|
|
'created',
|
|
'creator',
|
|
'tags',
|
|
'type',
|
|
'list',
|
|
'caption',
|
|
'modifier',
|
|
'wiki',
|
|
'customFields',
|
|
'bag',
|
|
'revision'
|
|
],
|
|
#category : #'TiddlyWiki-Model'
|
|
}
|
|
|
|
{ #category : #'instance creation' }
|
|
Tiddler class >> nowLocal [
|
|
^ ((ZTimestampFormat fromString: '20010203160506700')
|
|
format: (ZTimestamp fromString: Time nowLocal asDateAndTime asString)) copyFrom: 1 to: 17
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> appendPrefixToText: prefix [
|
|
|
|
| textPre |
|
|
textPre := self text.
|
|
self text: prefix, textPre.
|
|
^ self
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> asDictionary [
|
|
| response |
|
|
response := Dictionary new
|
|
at: 'title' put: self title;
|
|
at: 'text' put: self text;
|
|
at: 'created' put: self created;
|
|
at: 'tags' put: self tagsAsString;
|
|
at: 'type' put: self type;
|
|
at: 'creator' put: self creator;
|
|
at: 'modifier' put: self modifier;
|
|
at: 'modified' put: self modified;
|
|
at: 'bag' put: self bag;
|
|
at: 'revision' put: self revision;
|
|
at: 'caption' put: self caption;
|
|
yourself.
|
|
self customFields ifNotEmpty: [
|
|
self customFields keysAndValuesDo: [:k :v |
|
|
response at: k put: v
|
|
]
|
|
].
|
|
^ response
|
|
]
|
|
|
|
{ #category : #converting }
|
|
Tiddler >> asJson [
|
|
^ STONJSON toStringPretty: { self asDictionary }
|
|
]
|
|
|
|
{ #category : #converting }
|
|
Tiddler >> asJsonString [
|
|
^ STONJSON toStringPretty: self asDictionary
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> asJsonTempFile [
|
|
^ MarkupFile exportAsFileOn: FileLocator temp / self title, 'json' containing:self asJson
|
|
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> asStonStringPretty [
|
|
| output temp |
|
|
temp := self copy.
|
|
temp wiki: nil.
|
|
output := '' writeStream.
|
|
(STON writer on: output)
|
|
newLine: String lf;
|
|
prettyPrint: true;
|
|
keepNewLines: true;
|
|
nextPut: temp.
|
|
^ output contents
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> bag [
|
|
^ bag
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> bag: aString [
|
|
bag := aString
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> caption [
|
|
|
|
^ caption
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> caption: anObject [
|
|
|
|
caption := anObject
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> created [
|
|
|
|
^ created ifNil: [ created := self class nowLocal ]
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> created: anObject [
|
|
|
|
created := anObject
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> createdAsTWFormat [
|
|
(self created beginsWith: 'NaN') ifTrue: [ self created: self class nowLocal].
|
|
^ ((ZTimestampFormat fromString: '20010203160506700')
|
|
format: (ZTimestamp fromString: self created)) copyFrom: 1 to: 17
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> createdReversableEncoded [
|
|
"I encode the tiddler creation date with miliseconds precision in a shorter reversable way
|
|
(from 17 characters to 10).
|
|
But when tiddlers are created with the same exact date (for example programmatically)
|
|
I produce the same encoding (because of reversability).
|
|
I recommend to use nanoID instead to get unique visible different identifiers "
|
|
| output longDate |
|
|
longDate := self createdAsTWFormat.
|
|
output := WriteStream on: ''.
|
|
1 to: 14 by: 2 do: [ :i |
|
|
output nextPutAll: (longDate copyFrom: i to: i +1) greaseInteger asCharacterDigit greaseString
|
|
].
|
|
output nextPutAll: (longDate copyFrom: 15 to: 17).
|
|
^ output contents
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> creationTime [
|
|
^ ZTimestamp fromString: self created
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> creator [
|
|
|
|
^ creator
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> creator: anObject [
|
|
|
|
creator := anObject
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> customFields [
|
|
^ customFields ifNil: [ customFields := Dictionary new]
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> customFieldsWithMediaLinks [
|
|
| response |
|
|
response := OrderedDictionary new.
|
|
self customFields keysAndValuesDo: [:k :v |
|
|
(v endsWithAnyOf: #('mp4' 'wav' 'jpg' 'jpeg' 'png'))
|
|
ifTrue: [response at: k put: v ]
|
|
].
|
|
^ response
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> deleteUid [
|
|
self customFields deleteKey: 'uid'.
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> downloadAndRelinkExternalMedia [
|
|
| mediaExtensions|
|
|
mediaExtensions := Dictionary new
|
|
at: 'audio' put: #('wav');
|
|
at: 'video' put: #('mp4');
|
|
at: 'image' put: #('jpg' 'jpeg' 'png');
|
|
yourself.
|
|
self customFieldsWithMediaLinks
|
|
keysAndValuesDo: [ :k :v |
|
|
mediaExtensions keysAndValuesDo: [:kind :extensions |
|
|
(v asLowercase endsWithAnyOf: extensions)
|
|
ifTrue: [ | localFile|
|
|
self downloadLink: v for: k into: 'external/', kind , '/'.
|
|
localFile := (self wiki substitutions at: self title at: k) second.
|
|
self customFields at: k put: localFile.
|
|
]
|
|
]
|
|
].
|
|
^ self wiki substitutions
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> downloadLink: v for: k [
|
|
| filePath fileName semiFilePath |
|
|
fileName := v asUrl segments last.
|
|
semiFilePath := 'external/video/' , fileName.
|
|
filePath := (self wiki folder / semiFilePath) fullName.
|
|
GtSubprocessWithInMemoryOutput new
|
|
shellCommand: 'curl -L -# ' , v , ' -o ' , filePath;
|
|
runAndWait;
|
|
stdout.
|
|
^ Dictionary new
|
|
at: k
|
|
put: {v.
|
|
semiFilePath};
|
|
yourself
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> downloadLink: v for: k into: subfolder [
|
|
| filePath fileName semiFilePath |
|
|
fileName := v asUrl segments last.
|
|
semiFilePath := subfolder , fileName.
|
|
filePath := (self wiki folder / semiFilePath) fullName.
|
|
GtSubprocessWithInMemoryOutput new
|
|
shellCommand: 'curl -L -# ' , v , ' -o ' , filePath;
|
|
runAndWait;
|
|
stdout.
|
|
^ self wiki substitutions
|
|
at: self title at: k put: {v. semiFilePath};
|
|
yourself
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> exportJSONFile [
|
|
|
|
| jsonFile folder |
|
|
folder := self wiki folder.
|
|
jsonFile := folder / 'tiddlers' / ((self fileName removeSuffix: '.ston'), '.json').
|
|
^ MarkupFile exportAsFileOn: jsonFile containing:self asJson
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> exportSTONFile [
|
|
|
|
^ self exportSTONFileInto: 'tiddlers'
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> exportSTONFileInto: subfolder [
|
|
|
|
| stonFile tiddlersSubfolder |
|
|
tiddlersSubfolder := self wiki folder / subfolder.
|
|
tiddlersSubfolder exists
|
|
ifFalse: [ tiddlersSubfolder ensureCreateDirectory ].
|
|
stonFile := self wiki folder / subfolder / self fileName.
|
|
^ MarkupFile exportAsFileOn: stonFile containing: self asStonStringPretty
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> exportSTONFileOptimized [
|
|
|
|
| exporter wikiFolder tiddlersFolder |
|
|
wikiFolder := self wiki folder.
|
|
exporter := MiniDocs appFolder / 'scripts' / 'stringAsFileInto'.
|
|
exporter exists ifFalse: [ self installNimFileExporter ].
|
|
tiddlersFolder := wikiFolder / 'tiddlers'.
|
|
tiddlersFolder exists ifFalse: [ tiddlersFolder ensureCreateDirectory ].
|
|
|
|
OSSUnixSubprocess new
|
|
command: exporter fullName;
|
|
arguments: { self asStonStringPretty . self fileName };
|
|
workingDirectory: tiddlersFolder fullName;
|
|
runAndWaitOnExitDo: [ :process :outString | ^ tiddlersFolder / self fileName ]
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> exportWithTemplate: aTemplate [
|
|
^ aTemplate asMustacheTemplate value: self asDictionary
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> fileName [
|
|
| dashedTitle sanitized |
|
|
dashedTitle := '-' join: (self title substrings collect: [ :each | each ]).
|
|
sanitized := dashedTitle copyWithoutAll: #($¿ $? $! $¡ $/).
|
|
^ sanitized , '--', (self uid copyFrom: 1 to: 12), '.ston'.
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> fromDictionary: aDictionary [
|
|
| customKeys |
|
|
self
|
|
title: (aDictionary at: 'title');
|
|
text: (aDictionary at: 'text' ifAbsentPut: [ nil ]);
|
|
tags: (aDictionary at: 'tags' ifAbsentPut: [ nil ]);
|
|
created: (aDictionary at: 'created' ifAbsentPut: [ self class nowLocal ]);
|
|
creator: (aDictionary at: 'creator' ifAbsentPut: [ nil ]);
|
|
modified: (aDictionary at: 'modified' ifAbsentPut: [ nil ]);
|
|
modifier: (aDictionary at: 'modifier' ifAbsentPut: [ nil ]);
|
|
type: (aDictionary at: 'type' ifAbsentPut: [ nil ]);
|
|
caption: (aDictionary at: 'caption' ifAbsentPut: [ nil ]);
|
|
bag: (aDictionary at: 'bag' ifAbsentPut: [ nil ]);
|
|
list: (aDictionary at: 'list' ifAbsentPut: [ nil ]);
|
|
revision: (aDictionary at: 'revision' ifAbsentPut: [ nil ]).
|
|
customKeys := aDictionary keys
|
|
copyWithoutAll: (self class instanceVariables collect: [ :each | each name ]).
|
|
"(customKeys includes: 'uid') ifFalse: [ self uidGenerator ]."
|
|
customKeys do: [:key | | valueTemp |
|
|
valueTemp := aDictionary at: key.
|
|
valueTemp class = Array
|
|
ifTrue: [ self customFields at: key put: (self tiddlersListFrom: valueTemp) ]
|
|
ifFalse: [ self customFields at: key put: valueTemp ].
|
|
valueTemp class
|
|
].
|
|
]
|
|
|
|
{ #category : #'instance creation' }
|
|
Tiddler >> fromMarkdownParsedItems: aCollection [
|
|
| outputStream |
|
|
outputStream := '' writeStream.
|
|
aCollection children do: [ :each |
|
|
each children
|
|
ifEmpty: [ self itemContentsStringFor: each into: outputStream ]
|
|
ifNotEmpty: [
|
|
each children do: [ :child |
|
|
self itemContentsStringFor: child into: outputStream ] ]
|
|
]
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> gtTextFor: aView [
|
|
<gtView>
|
|
^ aView textEditor
|
|
title: 'Text';
|
|
text: [ text ]
|
|
]
|
|
|
|
{ #category : #testing }
|
|
Tiddler >> hasUID [
|
|
|
|
^ self customFields includesKey: 'uid'
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> importFedWikiPage: pageViewUrlString [
|
|
| pageTitle pageViewUrl pageData |
|
|
pageViewUrl := pageViewUrlString asZnUrl.
|
|
pageTitle := pageViewUrl segments second.
|
|
pageData := (pageViewUrl scheme, '://', pageViewUrl host, '/', pageTitle, '.json') asZnUrl.
|
|
^ STONJSON fromString: pageData retrieveContents
|
|
]
|
|
|
|
{ #category : #testing }
|
|
Tiddler >> isImage [
|
|
|
|
^ self type ifNil: [ ^ false ];
|
|
beginsWith: 'image/'
|
|
]
|
|
|
|
{ #category : #testing }
|
|
Tiddler >> isJavascript [
|
|
|
|
^ self type = 'application/javascript'
|
|
]
|
|
|
|
{ #category : #testing }
|
|
Tiddler >> isMarkdown [
|
|
|
|
^ self type = 'text/x-markdown'
|
|
]
|
|
|
|
{ #category : #testing }
|
|
Tiddler >> isNilType [
|
|
|
|
^ self type = nil
|
|
]
|
|
|
|
{ #category : #testing }
|
|
Tiddler >> isPDF [
|
|
|
|
^ self type = 'application/pdf'
|
|
]
|
|
|
|
{ #category : #testing }
|
|
Tiddler >> isShadow [
|
|
|
|
^ self title beginsWith: '$:/'
|
|
]
|
|
|
|
{ #category : #testing }
|
|
Tiddler >> isTW5Type [
|
|
|
|
^ self type = 'text/vnd.tiddlywiki'
|
|
]
|
|
|
|
{ #category : #testing }
|
|
Tiddler >> isTextPlain [
|
|
|
|
^ self type = 'text/plain'
|
|
]
|
|
|
|
{ #category : #testing }
|
|
Tiddler >> isXTiddlerDictionary [
|
|
|
|
^ self type = 'application/x-tiddler-dictionary'
|
|
]
|
|
|
|
{ #category : #utilities }
|
|
Tiddler >> itemContentsStringFor: item into: stream [
|
|
stream
|
|
nextPutAll: item text;
|
|
nextPut: Character cr;
|
|
nextPut: Character cr
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> linkedTiddlers [
|
|
"At the begining we are going to introduce 'pureTiddlers' as thos included in the wiki which are not linked
|
|
via aliases. Future versions of this method sould included internal aliased tiddlers."
|
|
| pureTiddlersTitles |
|
|
self rawLinks ifNil: [ ^nil ].
|
|
pureTiddlersTitles := self rawLinks difference: self rawAliasedLinks.
|
|
^ self wiki tiddlers select: [:tiddler | pureTiddlersTitles includes: tiddler title ].
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> list [
|
|
|
|
^ list
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> list: anObject [
|
|
|
|
list := anObject
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> listedTiddlers [
|
|
"I export all tiddlers in the list field as an alphabetic collection.
|
|
Future versions should preserve the order in the list.
|
|
Notice that while '#list' only gives the titles of the listed tiddlers,
|
|
I return them as proper tiddler objects."
|
|
| remainList remainListArray listedTiddlers |
|
|
self list ifNil: [^ nil ].
|
|
remainList := self list copy.
|
|
self manualLinksList do: [:manualLink |
|
|
remainList := remainList copyReplaceAll: manualLink with: ''
|
|
].
|
|
remainListArray := (remainList copyReplaceAll: '[[]]' with: '') withBlanksCondensed splitOn: Character space.
|
|
listedTiddlers := self manualLinksList, remainListArray.
|
|
^ self wiki tiddlers select: [:tiddler | listedTiddlers includes: tiddler title ].
|
|
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> manualLinksList [
|
|
self list ifNil: [^ nil].
|
|
^ WikiTextGrammar new linkSea star parse: self list.
|
|
]
|
|
|
|
{ #category : #utilities }
|
|
Tiddler >> markdownLinksAsWikiText [
|
|
"I'm useful to convert _internal_ links between formats, as is a common pattern
|
|
found when migrating content from Markdown to TiddlyWiki's WikiText.
|
|
I DON'T work on external links. A better regex could be used for that.
|
|
See:
|
|
- https://davidwells.io/snippets/regex-match-markdown-links
|
|
- http://blog.michaelperrin.fr/2019/02/04/advanced-regular-expressions/"
|
|
| markdownLinks |
|
|
markdownLinks := (self text splitOn: Character space) select: [:each | each matchesRegex: '\[(.+)\)'].
|
|
markdownLinks ifEmpty: [^ self].
|
|
^ markdownLinks
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> modified [
|
|
|
|
^ modified
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> modified: anObject [
|
|
|
|
modified := anObject
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> modifier [
|
|
|
|
^ modifier
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> modifier: anObject [
|
|
|
|
modifier := anObject
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> printOn: aStream [
|
|
super printOn: aStream.
|
|
aStream
|
|
nextPutAll: '( ', self title, ' )'
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> rawAliasedLinks [
|
|
^ self rawLinks select: [ :each | each includesSubstring: '|' ]
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> rawLinks [
|
|
self text ifNil: [ ^ Set new ].
|
|
^ (WikiTextGrammar new linkSea star parse: self text) asSet
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> removeTag: aTag [
|
|
|
|
| tagsPre |
|
|
tagsPre := self tags.
|
|
self tags: ('' join: (tagsPre splitOn: aTag)).
|
|
^ self
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> renameExternalMediaLinksInto: relativePath [
|
|
"This first implementation only renames external media links in custom fields.
|
|
Further development should offer the possibility to rename also the external media
|
|
appearing in the tiddler's text."
|
|
self customFieldsWithMediaLinks keysAndValuesDo: [:customField :oldLink | | link name |
|
|
link := oldLink asUrl.
|
|
link hasScheme ifFalse: [^ self ].
|
|
name := link segments last copyWithoutAll: '-'.
|
|
name := name asDashedLowercase.
|
|
name := (relativePath, '/', NanoID generate, '--', name).
|
|
self customFields
|
|
at: customField, '--original' put: oldLink;
|
|
at: customField put: name
|
|
].
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> revision [
|
|
^ revision
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> revision: aNumberString [
|
|
revision := aNumberString
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> tags [
|
|
|
|
^ tags ifNil: [tags := Set new]
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> tags: anObject [
|
|
|
|
tags := anObject
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> tagsAsString [
|
|
| response |
|
|
self tags ifEmpty: [^ '' ].
|
|
response := '' writeStream.
|
|
self tags do: [:tag |
|
|
response nextPutAll: '[[', tag, ']]'
|
|
].
|
|
^ response contents
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> tagsReformating [
|
|
| response sanitized |
|
|
self tags class ~= ByteString ifTrue: [ ^ self ].
|
|
response := Set new.
|
|
sanitized := self tags trimLeft: [:char | char = $[ ].
|
|
sanitized := sanitized trimRight: [:char | char = $] ].
|
|
response addAll: (sanitized trimmed splitOn: Character space) asSet.
|
|
self tags: response.
|
|
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> text [
|
|
|
|
^ text
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> text: anObject [
|
|
|
|
text := anObject
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> tiddlersListFrom: anArray [
|
|
| output |
|
|
output := '' writeStream.
|
|
|
|
anArray doWithIndex: [:each :i |
|
|
output nextPutAll: '[[', each asString, ']]'.
|
|
i = anArray size ifFalse: [ output nextPutAll: Character space asString ].
|
|
].
|
|
^ output contents.
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> title [
|
|
|
|
^ title
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> title: anObject [
|
|
|
|
title := anObject
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> type [
|
|
|
|
^ type
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> type: anObject [
|
|
|
|
type := anObject
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> uid [
|
|
^ self customFields at: 'uid' ifAbsentPut: [ self uidGenerator ].
|
|
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> uidGenerator [
|
|
|
|
^ self customFields at: 'uid' put: NanoID generate.
|
|
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> wiki [
|
|
^ wiki
|
|
]
|
|
|
|
{ #category : #accessing }
|
|
Tiddler >> wiki: aTiddlyWiki [
|
|
wiki := aTiddlyWiki
|
|
]
|