" I model a Tiddler object in [TiddlyWiki](https://tiddlywiki.com/). I implement the standard fields as described in the standard documentation at: " 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 [ ^ 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 rawLinksIn: self text ] { #category : #accessing } Tiddler >> rawLinksIn: aText [ aText ifNil: [ ^ Set new ]. ^ (WikiTextGrammar new linkSea star parse: aText) 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 ]