Importer-Exporter.st 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. Smalltalk current createPackage: 'Importer-Exporter'!
  2. Object subclass: #ChunkParser
  3. instanceVariableNames: 'stream'
  4. package: 'Importer-Exporter'!
  5. !ChunkParser commentStamp!
  6. I am responsible for parsing aStream contents in the chunk format.
  7. ## API
  8. ChunkParser new
  9. stream: aStream;
  10. nextChunk!
  11. !ChunkParser methodsFor: 'accessing'!
  12. stream: aStream
  13. stream := aStream
  14. ! !
  15. !ChunkParser methodsFor: 'reading'!
  16. nextChunk
  17. "The chunk format (Smalltalk Interchange Format or Fileout format)
  18. is a trivial format but can be a bit tricky to understand:
  19. - Uses the exclamation mark as delimiter of chunks.
  20. - Inside a chunk a normal exclamation mark must be doubled.
  21. - A non empty chunk must be a valid Smalltalk expression.
  22. - A chunk on top level with a preceding empty chunk is an instruction chunk:
  23. - The object created by the expression then takes over reading chunks.
  24. This metod returns next chunk as a String (trimmed), empty String (all whitespace) or nil."
  25. | char result chunk |
  26. result := '' writeStream.
  27. [char := stream next.
  28. char notNil] whileTrue: [
  29. char = '!!' ifTrue: [
  30. stream peek = '!!'
  31. ifTrue: [stream next "skipping the escape double"]
  32. ifFalse: [^result contents trimBoth "chunk end marker found"]].
  33. result nextPut: char].
  34. ^nil "a chunk needs to end with !!"
  35. ! !
  36. !ChunkParser class methodsFor: 'not yet classified'!
  37. on: aStream
  38. ^self new stream: aStream
  39. ! !
  40. Object subclass: #Exporter
  41. instanceVariableNames: ''
  42. package: 'Importer-Exporter'!
  43. !Exporter commentStamp!
  44. I am responsible for outputting Amber code into a JavaScript string.
  45. The generated output is enough to reconstruct the exported data, including Smalltalk source code and other metadata.
  46. ## Use case
  47. I am typically used to save code outside of the Amber runtime (committing to disk, etc.).
  48. ## API
  49. Use `#exportAll`, `#exportClass:` or `#exportPackage:` methods.!
  50. !Exporter methodsFor: 'fileOut'!
  51. exportAll
  52. "Export all packages in the system."
  53. ^String streamContents: [:stream |
  54. Smalltalk current packages do: [:pkg |
  55. self exportPackage: pkg on: stream]]
  56. !
  57. exportClass: aClass on: aStream
  58. "Export a single class. Subclasses override these methods."
  59. self exportDefinitionOf: aClass on: aStream.
  60. self exportMethodsOf: aClass on: aStream.
  61. self exportMetaDefinitionOf: aClass on: aStream.
  62. self exportMethodsOf: aClass class on: aStream
  63. !
  64. exportPackage: package on: stream
  65. "Export a given package."
  66. self exportPackagePrologueOf: package on: stream.
  67. [
  68. self exportPackageDefinitionOf: package on: stream.
  69. "Export classes in dependency order.
  70. Update (issue #171): Remove duplicates for export"
  71. package sortedClasses asSet do: [:each |
  72. self exportClass: each on: stream ].
  73. self exportPackageExtensionsOf: package on: stream
  74. ] ensure: [
  75. self exportPackageEpilogueOf: package on: stream
  76. ]
  77. ! !
  78. !Exporter methodsFor: 'private'!
  79. classNameFor: aClass
  80. ^aClass isMetaclass
  81. ifTrue: [aClass instanceClass name, '.klass']
  82. ifFalse: [
  83. aClass isNil
  84. ifTrue: ['nil']
  85. ifFalse: [aClass name]]
  86. !
  87. exportDefinitionOf: aClass on: aStream
  88. aStream
  89. nextPutAll: 'smalltalk.addClass(';
  90. nextPutAll: '''', (self classNameFor: aClass), ''', ';
  91. nextPutAll: 'smalltalk.', (self classNameFor: aClass superclass);
  92. nextPutAll: ', ['.
  93. aClass instanceVariableNames
  94. do: [:each | aStream nextPutAll: '''', each, '''']
  95. separatedBy: [aStream nextPutAll: ', '].
  96. aStream
  97. nextPutAll: '], ''';
  98. nextPutAll: aClass category, '''';
  99. nextPutAll: ');'.
  100. aClass comment notEmpty ifTrue: [
  101. aStream
  102. lf;
  103. nextPutAll: 'smalltalk.';
  104. nextPutAll: (self classNameFor: aClass);
  105. nextPutAll: '.comment=';
  106. nextPutAll: aClass comment asJavascript;
  107. nextPutAll: ';'].
  108. aStream lf
  109. !
  110. exportMetaDefinitionOf: aClass on: aStream
  111. aClass class instanceVariableNames isEmpty ifFalse: [
  112. aStream
  113. nextPutAll: 'smalltalk.', (self classNameFor: aClass class);
  114. nextPutAll: '.iVarNames = ['.
  115. aClass class instanceVariableNames
  116. do: [:each | aStream nextPutAll: '''', each, '''']
  117. separatedBy: [aStream nextPutAll: ','].
  118. aStream nextPutAll: '];', String lf]
  119. !
  120. exportMethod: aMethod on: aStream
  121. aStream
  122. nextPutAll: 'smalltalk.addMethod(';lf;
  123. "nextPutAll: aMethod selector asSelector asJavascript, ',';lf;"
  124. nextPutAll: 'smalltalk.method({';lf;
  125. nextPutAll: 'selector: ', aMethod selector asJavascript, ',';lf;
  126. nextPutAll: 'category: ''', aMethod category, ''',';lf;
  127. nextPutAll: 'fn: ', aMethod fn compiledSource, ',';lf;
  128. nextPutAll: 'args: ', aMethod arguments asJavascript, ','; lf;
  129. nextPutAll: 'source: ', aMethod source asJavascript, ',';lf;
  130. nextPutAll: 'messageSends: ', aMethod messageSends asJavascript, ',';lf;
  131. nextPutAll: 'referencedClasses: ', aMethod referencedClasses asJavascript.
  132. aStream
  133. lf;
  134. nextPutAll: '}),';lf;
  135. nextPutAll: 'smalltalk.', (self classNameFor: aMethod methodClass);
  136. nextPutAll: ');';lf;lf
  137. !
  138. exportMethodsOf: aClass on: aStream
  139. "Issue #143: sort methods alphabetically"
  140. ((aClass methodDictionary values) sorted: [:a :b | a selector <= b selector]) do: [:each |
  141. (each category match: '^\*') ifFalse: [
  142. self exportMethod: each on: aStream]].
  143. aStream lf
  144. !
  145. exportPackageDefinitionOf: package on: aStream
  146. aStream
  147. nextPutAll: 'smalltalk.addPackage(';
  148. nextPutAll: '''', package name, ''');';
  149. lf
  150. !
  151. exportPackageEpilogueOf: aPackage on: aStream
  152. aStream
  153. nextPutAll: '})(global_smalltalk,global_nil,global__st);';
  154. lf
  155. !
  156. exportPackageExtensionsOf: package on: aStream
  157. "Issue #143: sort classes and methods alphabetically"
  158. | name |
  159. name := package name.
  160. (Package sortedClasses: Smalltalk current classes) do: [:each |
  161. {each. each class} do: [:aClass |
  162. ((aClass methodDictionary values) sorted: [:a :b | a selector <= b selector]) do: [:method |
  163. (method category match: '^\*', name) ifTrue: [
  164. self exportMethod: method on: aStream ]]]]
  165. !
  166. exportPackagePrologueOf: aPackage on: aStream
  167. aStream
  168. nextPutAll: '(function(smalltalk,nil,_st){';
  169. lf
  170. ! !
  171. Exporter subclass: #ChunkExporter
  172. instanceVariableNames: ''
  173. package: 'Importer-Exporter'!
  174. !ChunkExporter commentStamp!
  175. I am an exporter dedicated to outputting Amber source code in the classic Smalltalk chunk format.
  176. I do not output any compiled code.!
  177. !ChunkExporter methodsFor: 'private'!
  178. chunkEscape: aString
  179. "Replace all occurrences of !! with !!!! and trim at both ends."
  180. ^(aString replace: '!!' with: '!!!!') trimBoth
  181. !
  182. classNameFor: aClass
  183. ^aClass isMetaclass
  184. ifTrue: [aClass instanceClass name, ' class']
  185. ifFalse: [
  186. aClass isNil
  187. ifTrue: ['nil']
  188. ifFalse: [aClass name]]
  189. !
  190. exportCategory: category on: aStream
  191. "Issue #143: sort methods alphabetically"
  192. aStream
  193. nextPutAll: '!!', (self classNameFor: (category at: #class));
  194. nextPutAll: ' methodsFor: ''', (category at: #name), '''!!'.
  195. ((category at: #methods) sorted: [:a :b | a selector <= b selector]) do: [:each |
  196. self exportMethod: each on: aStream].
  197. aStream nextPutAll: ' !!'; lf; lf
  198. !
  199. exportDefinitionOf: aClass on: aStream
  200. "Chunk format."
  201. aStream
  202. nextPutAll: (self classNameFor: aClass superclass);
  203. nextPutAll: ' subclass: #', (self classNameFor: aClass); lf;
  204. tab; nextPutAll: 'instanceVariableNames: '''.
  205. aClass instanceVariableNames
  206. do: [:each | aStream nextPutAll: each]
  207. separatedBy: [aStream nextPutAll: ' '].
  208. aStream
  209. nextPutAll: ''''; lf;
  210. tab; nextPutAll: 'package: ''', aClass category, '''!!'; lf.
  211. aClass comment notEmpty ifTrue: [
  212. aStream
  213. nextPutAll: '!!', (self classNameFor: aClass), ' commentStamp!!';lf;
  214. nextPutAll: (self chunkEscape: aClass comment), '!!';lf].
  215. aStream lf
  216. !
  217. exportMetaDefinitionOf: aClass on: aStream
  218. aClass class instanceVariableNames isEmpty ifFalse: [
  219. aStream
  220. nextPutAll: (self classNameFor: aClass class);
  221. nextPutAll: ' instanceVariableNames: '''.
  222. aClass class instanceVariableNames
  223. do: [:each | aStream nextPutAll: each]
  224. separatedBy: [aStream nextPutAll: ' '].
  225. aStream
  226. nextPutAll: '''!!'; lf; lf]
  227. !
  228. exportMethod: aMethod on: aStream
  229. aStream
  230. lf; lf; nextPutAll: (self chunkEscape: aMethod source); lf;
  231. nextPutAll: '!!'
  232. !
  233. exportMethodsOf: aClass on: aStream
  234. "Issue #143: sort protocol alphabetically"
  235. | map |
  236. map := Dictionary new.
  237. aClass protocolsDo: [:category :methods |
  238. (category match: '^\*') ifFalse: [ map at: category put: methods ]].
  239. (map keys sorted: [:a :b | a <= b ]) do: [:category | | methods |
  240. methods := map at: category.
  241. self
  242. exportCategory: #{
  243. 'methods'->methods.
  244. 'name'->category.
  245. 'class'->aClass }
  246. on: aStream ]
  247. !
  248. exportPackageDefinitionOf: package on: aStream
  249. "Chunk format."
  250. aStream
  251. nextPutAll: 'Smalltalk current createPackage: ''', package name, '''!!';
  252. lf
  253. !
  254. exportPackageEpilogueOf: aPackage on: aStream.
  255. !
  256. exportPackageExtensionsOf: package on: aStream
  257. "We need to override this one too since we need to group
  258. all methods in a given protocol under a leading methodsFor: chunk
  259. for that class."
  260. "Issue #143: sort protocol alphabetically"
  261. | name map |
  262. name := package name.
  263. (Package sortedClasses: Smalltalk current classes) do: [:each |
  264. {each. each class} do: [:aClass |
  265. map := Dictionary new.
  266. aClass protocolsDo: [:category :methods |
  267. (category match: '^\*', name) ifTrue: [ map at: category put: methods ]].
  268. (map keys sorted: [:a :b | a <= b ]) do: [:category | | methods |
  269. methods := map at: category.
  270. self exportCategory: #{ 'methods'->methods. 'name'->category. 'class'->aClass} on: aStream ]]]
  271. !
  272. exportPackagePrologueOf: aPackage on: aStream.
  273. ! !
  274. Exporter subclass: #StrippedExporter
  275. instanceVariableNames: ''
  276. package: 'Importer-Exporter'!
  277. !StrippedExporter commentStamp!
  278. I export Amber code into a JavaScript string, but without any optional associated data like the Amber source code.!
  279. !StrippedExporter methodsFor: 'private'!
  280. exportDefinitionOf: aClass on: aStream
  281. aStream
  282. nextPutAll: 'smalltalk.addClass(';
  283. nextPutAll: '''', (self classNameFor: aClass), ''', ';
  284. nextPutAll: 'smalltalk.', (self classNameFor: aClass superclass);
  285. nextPutAll: ', ['.
  286. aClass instanceVariableNames
  287. do: [:each | aStream nextPutAll: '''', each, '''']
  288. separatedBy: [aStream nextPutAll: ', '].
  289. aStream
  290. nextPutAll: '], ''';
  291. nextPutAll: aClass category, '''';
  292. nextPutAll: ');'.
  293. aStream lf
  294. !
  295. exportMethod: aMethod on: aStream
  296. aStream
  297. nextPutAll: 'smalltalk.addMethod(';lf;
  298. "nextPutAll: aMethod selector asSelector asJavascript, ',';lf;"
  299. nextPutAll: 'smalltalk.method({';lf;
  300. nextPutAll: 'selector: ', aMethod selector asJavascript, ',';lf;
  301. nextPutAll: 'fn: ', aMethod fn compiledSource, ',';lf;
  302. nextPutAll: 'messageSends: ', aMethod messageSends asJavascript;
  303. nextPutAll: '}),';lf;
  304. nextPutAll: 'smalltalk.', (self classNameFor: aMethod methodClass);
  305. nextPutAll: ');';lf;lf
  306. ! !
  307. Object subclass: #Importer
  308. instanceVariableNames: ''
  309. package: 'Importer-Exporter'!
  310. !Importer commentStamp!
  311. I can import Amber code from a string in the chunk format.
  312. ## API
  313. Importer new import: aString!
  314. !Importer methodsFor: 'fileIn'!
  315. import: aStream
  316. | chunk result parser lastEmpty |
  317. parser := ChunkParser on: aStream.
  318. lastEmpty := false.
  319. [chunk := parser nextChunk.
  320. chunk isNil] whileFalse: [
  321. chunk isEmpty
  322. ifTrue: [lastEmpty := true]
  323. ifFalse: [
  324. result := Compiler new evaluateExpression: chunk.
  325. lastEmpty
  326. ifTrue: [
  327. lastEmpty := false.
  328. result scanFrom: parser]]]
  329. ! !
  330. Object subclass: #PackageHandler
  331. instanceVariableNames: ''
  332. package: 'Importer-Exporter'!
  333. !PackageHandler commentStamp!
  334. I am responsible for handling package loading and committing.
  335. I should not be used directly. Instead, use the corresponding `Package` methods.!
  336. !PackageHandler methodsFor: 'committing'!
  337. commit: aPackage
  338. {
  339. Exporter -> (aPackage commitPathJs, '/', aPackage name, '.js').
  340. StrippedExporter -> (aPackage commitPathJs, '/', aPackage name, '.deploy.js').
  341. ChunkExporter -> (aPackage commitPathSt, '/', aPackage name, '.st')
  342. }
  343. do: [ :commitStrategy|| fileContents |
  344. fileContents := String streamContents: [ :stream |
  345. commitStrategy key new exportPackage: aPackage on: stream ].
  346. self ajaxPutAt: commitStrategy value data: fileContents ]
  347. displayingProgress: 'Committing package ', aPackage name
  348. ! !
  349. !PackageHandler methodsFor: 'loading'!
  350. loadPackage: packageName prefix: aString
  351. | url |
  352. url := '/', aString, '/js/', packageName, '.js'.
  353. jQuery
  354. ajax: url
  355. options: #{
  356. 'type' -> 'GET'.
  357. 'dataType' -> 'script'.
  358. 'complete' -> [ :jqXHR :textStatus |
  359. jqXHR readyState = 4
  360. ifTrue: [ self setupPackageNamed: packageName prefix: aString ] ].
  361. 'error' -> [ window alert: 'Could not load package at: ', url ]
  362. }
  363. !
  364. loadPackages: aCollection prefix: aString
  365. aCollection do: [ :each |
  366. self loadPackage: each prefix: aString ]
  367. ! !
  368. !PackageHandler methodsFor: 'private'!
  369. ajaxPutAt: aURL data: aString
  370. jQuery
  371. ajax: aURL
  372. options: #{
  373. 'type' -> 'PUT'.
  374. 'data' -> aString.
  375. 'contentType' -> 'text/plain;charset=UTF-8'.
  376. 'error' -> [ :xhr | self error: 'Commiting ' , aURL , ' failed with reason: "' , (xhr responseText) , '"'] }
  377. !
  378. setupPackageNamed: packageName prefix: aString
  379. (Package named: packageName)
  380. setupClasses;
  381. commitPathJs: '/', aString, '/js';
  382. commitPathSt: '/', aString, '/st'
  383. ! !
  384. !PackageHandler class methodsFor: 'loading'!
  385. loadPackages: aCollection prefix: aString
  386. ^ self new loadPackages: aCollection prefix: aString
  387. ! !
  388. !Package methodsFor: '*Importer-Exporter'!
  389. commit
  390. ^ PackageHandler new commit: self
  391. ! !