├── CHANGELOG ├── LICENSE ├── README.md ├── bin └── scorm ├── examples └── example │ ├── README │ ├── adlcp_rootv1p2.xsd │ ├── example.html │ ├── images │ └── large │ │ └── example.png │ ├── ims_xml.xsd │ ├── imscp_rootv1p1p2.xsd │ ├── imsmanifest.xml │ └── imsmd_rootv1p2p1.xsd ├── lib ├── scorm.rb └── scorm │ ├── command.rb │ ├── commands │ ├── base.rb │ ├── bundle.rb │ ├── check.rb │ ├── create.rb │ ├── extract.rb │ ├── help.rb │ └── version.rb │ ├── datatypes.rb │ ├── manifest.rb │ ├── metadata.rb │ ├── organization.rb │ ├── package.rb │ └── resource.rb ├── scorm.gemspec └── skeleton ├── adlcp_rootv1p2.xsd ├── ims_xml.xsd ├── imscp_rootv1p1p2.xsd ├── imsmanifest.xml └── imsmd_rootv1p2p1.xsd /CHANGELOG: -------------------------------------------------------------------------------- 1 | *1.0.2* 2 | 3 | * The fix to Scorm::Package#package? didn't work as intended, tried another 4 | solution. 5 | 6 | *1.0.1* 7 | 8 | * Fixed bug in the "scorm extract" command that caused the newly extracted 9 | package to be cleaned up directly after it was extracted. 10 | 11 | * Fixed bug in Scorm::Package#package? that caused it to not properly identify 12 | zip files as packages. 13 | 14 | *1.0.0* 15 | 16 | * First public release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006-2010 Niklas Holmgren, Mindset AB 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SCORM is a Ruby library for reading and extracting Shareable Content Object 2 | Reference Model (SCORM) files. SCORM is a standardized package format used 3 | mainly by e-learning software to help with the exchange of course material 4 | between systems in an interoperable way. This gem supports SCORM 1.2 and SCORM 5 | 2004. 6 | 7 | The gem can both be used as a library in your application or as a command line 8 | tool. This gem does NOT handle the run-time part of the SCORM standard, only 9 | the so called "Content Aggregation Model". 10 | 11 | 12 | Usage 13 | ----- 14 | 15 | create # create a new package skeleton 16 | bundle []# creates a package from the current directory 17 | check # runs a test suite against your package 18 | extract # extracts and checks the specified package 19 | 20 | 21 | Example Workflow 22 | ---------------- 23 | 24 | scorm create mypackage # Create a new package skeleton 25 | cd mypackage # 26 | scorm bundle # Create .zip file from current directory 27 | scorm check mypackage.zip # Verify that the package is valid SCORM 28 | scorm extract mypackage.zip # Extract the mypackage.zip package 29 | 30 | 31 | Installation 32 | ------------ 33 | 34 | gem install scorm 35 | 36 | To use the SCORM gem as a library in your application to extract and read 37 | SCORM files you can simply require the 'scorm/package' file. 38 | 39 | require 'scorm/package' 40 | 41 | Scorm::Package.open('mypackage.zip') do |pkg| 42 | # Read stuff from the package... 43 | puts pkg.manifest.identifier 44 | puts pkg.manifest.default_organization.title 45 | puts pkg.manifest.metadata.general.title.value 46 | pkg.manifest.resources.each do |resource| 47 | puts resource.href 48 | puts resource.scorm_type 49 | if pkg.exists?(resource.files.first) 50 | puts resource.files.first 51 | puts pkg.file(resource.files.first) 52 | end 53 | end 54 | # etc... 55 | end 56 | 57 | See the RDOC documentation for more info about what you can read from the 58 | package manifest. 59 | 60 | 61 | About 62 | ----- 63 | 64 | Created and maintained by Niklas Holmgren. 65 | Released under the MIT license. http://github.com/mindset/scorm 66 | -------------------------------------------------------------------------------- /bin/scorm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib = File.expand_path(File.dirname(__FILE__) + '/../lib') 4 | $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib) 5 | 6 | require 'scorm' 7 | require 'scorm/command' 8 | 9 | args = ARGV.dup 10 | ARGV.clear 11 | command = args.shift.strip rescue 'help' 12 | 13 | Scorm::Command.run(command, args) -------------------------------------------------------------------------------- /examples/example/README: -------------------------------------------------------------------------------- 1 | This is an example of a very basic SCORM package. It contains one SCO (example.html) and one Asset (this file) and a total of three files: 2 | 3 | - example.html 4 | - images/large/example.png 5 | - README 6 | 7 | In addition to these it also contains an example manifest (imsmanifest.xml) 8 | and a set of required XML schema files (adlcp_rootv1p2.xsd, ims_xml.xsd, 9 | imscp_rootv1p1p2.xsd, imsmd_rootv1p2p1.xsd). 10 | 11 | The README file is not explicitly specified in the manifest as an asset, but 12 | is automatically included by the "scorm bundle" command. 13 | 14 | The package is compatible with the "Content Aggregation Model" in SCORM 2004 15 | 3rd Edition. -------------------------------------------------------------------------------- /examples/example/adlcp_rootv1p2.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /examples/example/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 5 | 6 | 7 |

Example

8 | Example Resource 9 | 10 | -------------------------------------------------------------------------------- /examples/example/images/large/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindset/scorm/b1cb060c1e38d215231bb50829e9cc686c2b5710/examples/example/images/large/example.png -------------------------------------------------------------------------------- /examples/example/ims_xml.xsd: -------------------------------------------------------------------------------- 1 | In namespace-aware XML processors, the "xml" prefix is bound to the namespace name http://www.w3.org/XML/1998/namespace. Do not reference this file in XML instances Schawn Thropp: Changed the uriReference type to string type Refers to universal XML 1.0 lang attribute Refers to XML Base: http://www.w3.org/TR/xmlbase -------------------------------------------------------------------------------- /examples/example/imscp_rootv1p1p2.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | DRAFT XSD for IMS Content Packaging version 1.1 DRAFT 20 | Copyright (c) 2001 IMS GLC, Inc. 21 | 2000-04-21, Adjustments by T.D. Wason from CP 1.0. 22 | 2001-02-22, T.D.Wason: Modify for 2000-10-24 XML-Schema version. Modified to support extension. 23 | 2001-03-12, T.D.Wason: Change filename, target and meta-data namespaces and meta-data fielname. Add meta-data to itemType, fileType and organizationType. 24 | Do not define namespaces for xml in XML instances generated from this xsd. 25 | Imports IMS meta-data xsd, lower case element names. 26 | This XSD provides a reference to the IMS meta-data root element as imsmd:record 27 | If the IMS meta-data is to be used in the XML instance then the instance must define an IMS meta-data prefix with a namespace. The meta-data targetNamespace should be used. 28 | 2001-03-20, Thor Anderson: Remove manifestref, change resourceref back to identifierref, change manifest back to contained by manifest. --Tom Wason: manifest may contain _none_ or more manifests. 29 | 2001-04-13 Tom Wason: corrected attirbute name structure. Was misnamed type. 30 | 2001-05-14 Schawn Thropp: Made all complexType extensible with the group.any 31 | Added the anyAttribute to all complexTypes. Changed the href attribute on the fileType and resourceType to xsd:string 32 | Changed the maxLength of the href, identifierref, parameters, structure attributes to match the Information model. 33 | 2001-07-25 Schawn Thropp: Changed the namespace for the Schema of Schemas to the 5/2/2001 W3C XML Schema 34 | Recommendation. attributeGroup attr.imsmd deleted, was not used anywhere. Any attribute declarations that have 35 | use = "default" changed to use="optional" - attr.structure.req. 36 | Any attribute declarations that have value="somevalue" changed to default="somevalue", 37 | attr.structure.req (hierarchical). Removed references to IMS MD Version 1.1. 38 | Modified attribute group "attr.resourcetype.req" to change use from optional 39 | to required to match the information model. As a result the default value also needed to be removed 40 | Name change for XSD. Changed to match version of CP Spec 41 | 42 | 43 | 44 | Inclusions and Imports 45 | 46 | 47 | 48 | 49 | 50 | Attribute Declarations 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | element groups 158 | 159 | 160 | 161 | 162 | Any namespaced element from any namespace may be included within an "any" element. The namespace for the imported element must be defined in the instance, and the schema must be imported. 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | -------------------------------------------------------------------------------- /examples/example/imsmanifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ADL SCORM 5 | 1.2 6 | 7 | 8 | 9 | <langstring xml:lang="x-none">Example</langstring> 10 | 11 | 12 | 13 | 14 | 1.0 15 | 16 | 17 | 18 | LOMv1.0 19 | 20 | 21 | Final 22 | 23 | 24 | 25 | 26 | ADL SCORM 1.2 27 | 28 | 29 | 30 | 31 | 32 | Example 33 | 34 | Example 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/example/imsmd_rootv1p2p1.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 2001-04-26 T.D.Wason. IMS meta-data 1.2 XML-Schema. 18 | 2001-06-07 S.E.Thropp. Changed the multiplicity on all elements to match the 19 | Final 1.2 Binding Specification. 20 | Changed all elements that use the langstringType to a multiplicy of 1 or more 21 | Changed centity in the contribute element to have a multiplicity of 0 or more. 22 | Changed the requirement element to have a multiplicity of 0 or more. 23 | 2001-07-25 Schawn Thropp. Updates to bring the XSD up to speed with the W3C 24 | XML Schema Recommendation. The following changes were made: Change the 25 | namespace to reference the 5/2/2001 W3C XML Schema Recommendation,the base 26 | type for the durtimeType, simpleType, was changed from timeDuration to duration. 27 | Any attribute declarations that have use="default" had to change to use="optional" 28 | - attr.type. Any attribute declarations that have value ="somevalue" had to change 29 | to default = "somevalue" - attr.type (URI) 30 | 2001-09-04 Schawn Thropp 31 | Changed the targetNamespace and namespace of schema to reflect version change 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Any namespaced element from any namespace may be used for an "any" element. The namespace for the imported element must be defined in the instance, and the schema must be imported. 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | -------------------------------------------------------------------------------- /lib/scorm.rb: -------------------------------------------------------------------------------- 1 | module Scorm 2 | VERSION = '1.0.2' 3 | end 4 | 5 | require 'scorm/package' -------------------------------------------------------------------------------- /lib/scorm/command.rb: -------------------------------------------------------------------------------- 1 | require 'scorm/commands/base' 2 | 3 | Dir["#{File.dirname(__FILE__)}/commands/*.rb"].each { |c| require c } 4 | 5 | module Scorm 6 | module Command 7 | class InvalidCommand < RuntimeError; end 8 | class CommandFailed < RuntimeError; end 9 | 10 | class << self 11 | 12 | def error(msg) 13 | STDERR.puts(msg) 14 | exit 1 15 | end 16 | 17 | def run(command, args) 18 | begin 19 | run_internal(command, args.dup) 20 | rescue Zip::ZipError => e 21 | error e.message 22 | rescue InvalidPackage => e 23 | error e.message 24 | rescue InvalidManifest => e 25 | error e.message 26 | rescue InvalidCommand 27 | error "Unknown command. Run 'scorm help' for usage information." 28 | rescue CommandFailed => e 29 | error e.message 30 | rescue Interrupt => e 31 | error "\n[canceled]" 32 | end 33 | end 34 | 35 | def run_internal(command, args) 36 | klass, method = parse(command) 37 | runner = klass.new(args) 38 | raise InvalidCommand unless runner.respond_to?(method) 39 | runner.send(method) 40 | end 41 | 42 | def parse(command) 43 | parts = command.split(':') 44 | case parts.size 45 | when 1 46 | begin 47 | return eval("Scorm::Command::#{command.capitalize}"), :index 48 | rescue NameError, NoMethodError 49 | raise InvalidCommand 50 | end 51 | when 2 52 | begin 53 | return Scorm::Command.const_get(parts[0].capitalize), parts[1] 54 | rescue NameError 55 | raise InvalidCommand 56 | end 57 | else 58 | raise InvalidCommand 59 | end 60 | end 61 | end 62 | end 63 | end -------------------------------------------------------------------------------- /lib/scorm/commands/base.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Scorm::Command 4 | class Base 5 | attr_accessor :args 6 | attr_reader :autodetected_package 7 | 8 | def initialize(args) 9 | @args = args 10 | @autodetected_package = false 11 | end 12 | 13 | def display(msg, newline=true) 14 | if newline 15 | puts(msg) 16 | else 17 | print(msg) 18 | STDOUT.flush 19 | end 20 | end 21 | 22 | def error(msg) 23 | STDERR.puts(msg) 24 | exit 1 25 | end 26 | 27 | def extract_package(force=true) 28 | package = extract_option('--package', false) 29 | raise(CommandFailed, "You must specify a package name after --package") if package == false 30 | unless package 31 | raise(CommandFailed, "No package specified.\nRun this command from package folder or set it adding --package ") if force 32 | @autodetected_package = true 33 | end 34 | package 35 | end 36 | 37 | def extract_option(options, default=true) 38 | values = options.is_a?(Array) ? options : [options] 39 | return unless opt_index = args.select { |a| values.include? a }.first 40 | opt_position = args.index(opt_index) + 1 41 | if args.size > opt_position && opt_value = args[opt_position] 42 | if opt_value.include?('--') 43 | opt_value = nil 44 | else 45 | args.delete_at(opt_position) 46 | end 47 | end 48 | opt_value ||= default 49 | args.delete(opt_index) 50 | block_given? ? yield(opt_value) : opt_value 51 | end 52 | end 53 | end -------------------------------------------------------------------------------- /lib/scorm/commands/bundle.rb: -------------------------------------------------------------------------------- 1 | module Scorm::Command 2 | class Bundle < Base 3 | def index 4 | name = args.shift.strip rescue '.' 5 | unless File.exist?(File.join(File.expand_path(name), 'imsmanifest.xml')) 6 | raise(CommandFailed, "Invalid package, didn't find any imsmanifest.xml file.") 7 | end 8 | 9 | outname = File.basename(File.expand_path(name)) + '.zip' 10 | 11 | require 'zip/zip' 12 | Zip::ZipFile.open(outname, Zip::ZipFile::CREATE) do |zipfile| 13 | Scorm::Package.open(name) do |pkg| 14 | Scorm::Manifest::MANIFEST_FILES.each do |file| 15 | zipfile.get_output_stream(file) {|f| f.write(pkg.file(file)) } 16 | display file 17 | end 18 | files = pkg.manifest.resources.map {|r| r.files }.flatten.uniq 19 | files.each do |file| 20 | zipfile.get_output_stream(file) {|f| f.write(pkg.file(file)) } 21 | display file 22 | end 23 | end 24 | end 25 | 26 | display "Created new SCORM package \"#{outname}\"." 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /lib/scorm/commands/check.rb: -------------------------------------------------------------------------------- 1 | module Scorm::Command 2 | class Check < Base 3 | def index 4 | package = args.shift.strip rescue '' 5 | raise(CommandFailed, "Invalid package.") if package == '' 6 | 7 | Scorm::Package.open(package, :dry_run => true) do |pkg| 8 | display "Checking package \"#{File.basename(package)}\"" 9 | display "" 10 | display "== UUID ==" 11 | display "Identifier: #{pkg.manifest.identifier}" 12 | display "" 13 | display "== Manifest ==" 14 | Scorm::Manifest::MANIFEST_FILES.each do |file| 15 | if pkg.exists?(file) 16 | display "#{file} -> OK" 17 | else 18 | display "#{file} -> Missing" 19 | end 20 | end 21 | display "" 22 | display "== Organizations ==" 23 | pkg.manifest.organizations.each do |id, organization| 24 | if organization == pkg.manifest.default_organization 25 | display "#{organization.title} (default)" 26 | else 27 | display "#{organization.title}" 28 | end 29 | end 30 | display "" 31 | display "== Resources ==" 32 | pkg.manifest.resources.each do |resource| 33 | display "#{resource.href} (#{resource.type}, #{resource.scorm_type}):" 34 | resource.files.each do |file| 35 | if pkg.exists?(file) 36 | display " - #{file} -> OK" 37 | else 38 | display " - #{file} -> Missing" 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /lib/scorm/commands/create.rb: -------------------------------------------------------------------------------- 1 | module Scorm::Command 2 | class Create < Base 3 | def index 4 | name = args.shift.strip rescue '' 5 | raise(CommandFailed, "Invalid package name.") if name == '' 6 | 7 | FileUtils.mkdir_p(name) 8 | Dir.glob(File.join(File.dirname(File.expand_path(__FILE__)), '../../../skeleton/*')).each do |file| 9 | FileUtils.cp(file, name) 10 | display "#{name}/#{File.basename(file)}" 11 | end 12 | display "Created new SCORM package \"#{name}\"." 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /lib/scorm/commands/extract.rb: -------------------------------------------------------------------------------- 1 | module Scorm::Command 2 | class Extract < Base 3 | def index 4 | package = args.shift.strip rescue '' 5 | raise(CommandFailed, "Invalid package.") if package == '' 6 | 7 | Scorm::Package.open(package, :cleanup => false) do |pkg| 8 | display "Extracted package to #{pkg.path}" 9 | end 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /lib/scorm/commands/help.rb: -------------------------------------------------------------------------------- 1 | module Scorm::Command 2 | class Help < Base 3 | class HelpGroup < Array 4 | attr_reader :title 5 | 6 | def initialize(title) 7 | @title = title 8 | end 9 | 10 | def command(name, description) 11 | self << [name, description] 12 | end 13 | 14 | def space 15 | self << ['', ''] 16 | end 17 | end 18 | 19 | def self.groups 20 | @groups ||= [] 21 | end 22 | 23 | def self.group(title, &block) 24 | groups << begin 25 | group = HelpGroup.new(title) 26 | yield group 27 | group 28 | end 29 | end 30 | 31 | def self.create_default_groups! 32 | group 'Commands' do |group| 33 | group.command 'help', 'show this usage' 34 | group.command 'version', 'show the gem version' 35 | group.space 36 | group.command 'create ', 'create a new package skeleton' 37 | group.command 'bundle []', 'creates a package from the current directory' 38 | group.command 'check ', 'runs a test suite against your package' 39 | group.command 'extract ', 'extracts and checks the specified package' 40 | end 41 | end 42 | 43 | def index 44 | display usage 45 | end 46 | 47 | def usage 48 | longest_command_length = self.class.groups.map do |group| 49 | group.map { |g| g.first.length } 50 | end.flatten.max 51 | 52 | self.class.groups.inject(StringIO.new) do |output, group| 53 | output.puts "=== %s" % group.title 54 | output.puts 55 | 56 | group.each do |command, description| 57 | if command.empty? 58 | output.puts 59 | else 60 | output.puts "%-*s # %s" % [longest_command_length, command, description] 61 | end 62 | end 63 | 64 | output.puts 65 | output 66 | end.string + <<-EOTXT 67 | === Example 68 | 69 | scorm create mypackage 70 | cd mypackage 71 | scorm check 72 | scorm bundle 73 | scorm extract mypackage.zip 74 | 75 | EOTXT 76 | end 77 | end 78 | end 79 | 80 | Scorm::Command::Help.create_default_groups! -------------------------------------------------------------------------------- /lib/scorm/commands/version.rb: -------------------------------------------------------------------------------- 1 | module Scorm::Command 2 | class Version < Base 3 | def index 4 | display Scorm::VERSION 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /lib/scorm/datatypes.rb: -------------------------------------------------------------------------------- 1 | module Scorm 2 | module Datatypes 3 | 4 | class Timeinterval 5 | def initialize(seconds) 6 | @sec = seconds 7 | end 8 | 9 | def self.parse(str) 10 | case str 11 | when /(\d{2,4}):(\d{2}):(\d{2})/ 12 | values = str.match(/(\d{2,4}):(\d{2}):(\d{2})/) 13 | hour = values[1].to_i 14 | minute = values[2].to_i 15 | second = values[3].to_i 16 | else 17 | date, time = str.split('T') 18 | if date 19 | year = date.match(/([0-9]+Y)/)[1].to_i if date.match(/([0-9]+Y)/) 20 | month = date.match(/([0-9]+M)/)[1].to_i if date.match(/([0-9]+M)/) 21 | day = date.match(/([0-9]+D)/)[1].to_i if date.match(/([0-9]+D)/) 22 | end 23 | if time 24 | hour = time.match(/([0-9]+H)/)[1].to_i if time.match(/([0-9]+H)/) 25 | minute = time.match(/([0-9]+M)/)[1].to_i if time.match(/([0-9]+M)/) 26 | second = time.match(/([0-9\.]+S)/)[1].to_f if time.match(/([0-9\.]+S)/) 27 | end 28 | end 29 | year = year || 0 30 | month = month || 0 31 | day = day || 0 32 | hour = hour || 0 33 | minute = minute || 0 34 | second = second || 0 35 | self.new((year*31557600) + (month*2629800) + (day*86400) + (hour*3600) + (minute*60) + second) 36 | end 37 | 38 | def to_i 39 | @sec.to_i 40 | end 41 | 42 | def to_f 43 | @sec.to_f 44 | end 45 | 46 | def to_s 47 | sec = self.to_i 48 | hours = (sec/60/60).to_i 49 | sec -= hours*60*60 50 | min = (sec/60).to_i 51 | sec -= min*60 52 | return "#{hours}:#{min}:#{sec}" 53 | end 54 | end 55 | 56 | end 57 | end -------------------------------------------------------------------------------- /lib/scorm/manifest.rb: -------------------------------------------------------------------------------- 1 | require 'rexml/document' 2 | require 'scorm/metadata' 3 | require 'scorm/organization' 4 | require 'scorm/resource' 5 | 6 | module Scorm 7 | class Manifest 8 | 9 | # Versions of the SCORM standard that is supported when running in 10 | # strict mode. When not running in strict mode, the library will not 11 | # care about the version specified in the package manifest and will 12 | # simply try its best to parse the information that it finds. 13 | SUPPORTED_VERSIONS = ['2004 3rd Edition', 'CAM 1.3', '1.2'] 14 | 15 | # List of XML and XML Schema files that are part of the manifest for 16 | # the package. 17 | MANIFEST_FILES = %w(imsmanifest.xml adlcp_rootv1p2.xsd ims_xml.xsd 18 | imscp_rootv1p1p2.xsd imsmd_rootv1p2p1.xsd) 19 | 20 | # Files that might be present in a package, but that should not be 21 | # interprested as resources. All files starting with a "." (i.e. hidden 22 | # files) is also implicitly included in this list. 23 | RESOURCES_BLACKLIST = [ 24 | '__MACOSX', 'desktop.ini', 'Thumbs.db' 25 | ].concat(MANIFEST_FILES) 26 | 27 | attr_accessor :identifier 28 | attr_accessor :metadata 29 | attr_accessor :organizations 30 | attr_accessor :default_organization 31 | attr_accessor :resources 32 | attr_accessor :base_url 33 | attr_accessor :schema 34 | attr_accessor :schema_version 35 | 36 | def initialize(package, manifest_data) 37 | @xmldoc = REXML::Document.new(manifest_data) 38 | 39 | @package = package 40 | @metadata = Scorm::Metadata.new 41 | @organizations = Hash.new 42 | @resources = Hash.new 43 | 44 | # Manifest identifier 45 | @identifier = @xmldoc.root.attribute('identifier').to_s 46 | 47 | # Read metadata 48 | if metadata_el = REXML::XPath.first(@xmldoc.root, '/manifest/metadata') 49 | # Read and 50 | schema_el = REXML::XPath.first(metadata_el, 'schema') 51 | schemaversion_el = REXML::XPath.first(metadata_el, 'schemaversion') 52 | @schema = schema_el.text.to_s unless schema_el.nil? 53 | @schema_version = schemaversion_el.text.to_s unless schemaversion_el.nil? 54 | 55 | if @package.options[:strict] 56 | if (@schema != 'ADL SCORM') || (!SUPPORTED_VERSIONS.include?(@schema_version)) 57 | raise InvalidManifest, "Sorry, unsupported SCORM-version (#{schema_el.text.to_s} #{schemaversion_el.text.to_s}), try turning strict parsing off." 58 | end 59 | end 60 | 61 | # Find a element... 62 | lom_el = nil 63 | if adlcp_location = REXML::XPath.first(metadata_el, 'adlcp:location') 64 | # Read external metadata file 65 | metadata_xmldoc = REXML::Document.new(package.file(adlcp_location.text.to_s)) 66 | if metadata_xmldoc.nil? || (metadata_xmldoc.root.name != 'lom') 67 | raise InvalidManifest, "Invalid external metadata file (#{adlcp_location.text.to_s})." 68 | else 69 | lom_el = metadata_xmldoc.root 70 | end 71 | else 72 | # Read inline metadata 73 | lom_el = REXML::XPath.first(metadata_el, 'lom') || 74 | REXML::XPath.first(metadata_el, 'lom:lom') 75 | end 76 | 77 | # Read lom metadata 78 | if lom_el 79 | @metadata = Scorm::Metadata.from_xml(lom_el) 80 | end 81 | end 82 | 83 | # Read organizations 84 | if organizations_el = REXML::XPath.first(@xmldoc.root, '/manifest/organizations') 85 | default_organization_id = organizations_el.attribute('default').to_s 86 | REXML::XPath.each(@xmldoc.root, '/manifest/organizations/organization') do |el| 87 | org = Scorm::Organization.from_xml(el) 88 | @organizations[org.id.to_s] = org 89 | end 90 | # Set the default organization 91 | @default_organization = @organizations[default_organization_id] 92 | raise InvalidManifest, "No default organization (#{default_organization_id})." if @default_organization.nil? 93 | else 94 | raise InvalidManifest, 'Missing organizations element.' 95 | end 96 | 97 | # Read resources 98 | REXML::XPath.each(@xmldoc.root, '/manifest/resources/resource') do |el| 99 | res = Scorm::Resource.from_xml(el) 100 | @resources[res.id] = res 101 | end 102 | 103 | # Read additional resources as assets (this is a fix for packages that 104 | # don't correctly specify all resource dependencies in the manifest). 105 | @package.files.each do |file| 106 | next if File.directory?(file) 107 | next if RESOURCES_BLACKLIST.include?(File.basename(file)) 108 | next if File.basename(file) =~ /^\./ 109 | next unless self.resources(:with_file => file).empty? 110 | next unless self.resources(:href => file).empty? 111 | 112 | res = Scorm::Resource.new(file, 'webcontent', 'asset', file, nil, [file]) 113 | @resources[file] = res 114 | end 115 | 116 | # Read (optional) base url for resources 117 | resources_el = REXML::XPath.first(@xmldoc.root, '/manifest/resources') 118 | @base_url = (resources_el.attribute('xml:base') || '').to_s 119 | 120 | # Read sub-manifests 121 | #REXML::XPath. 122 | end 123 | 124 | def resources(options = nil) 125 | if (options.nil?) || (!options.is_a?(Hash)) 126 | @resources.values 127 | else 128 | subset = @resources.values 129 | if options[:id] 130 | subset = subset.find_all {|r| r.id == options[:id].to_s } 131 | end 132 | if options[:type] 133 | subset = subset.find_all {|r| r.type == options[:type].to_s } 134 | end 135 | if options[:scorm_type] 136 | subset = subset.find_all {|r| r.scorm_type == options[:scorm_type].to_s } 137 | end 138 | if options[:href] 139 | subset = subset.find_all {|r| r.href == options[:href].to_s } 140 | end 141 | if options[:with_file] 142 | subset = subset.find_all {|r| r.files.include?(options[:with_file].to_s) } 143 | end 144 | subset 145 | end 146 | end 147 | 148 | def sco(item, attribute = nil) 149 | resource = self.resources(:id => item.resource_id).first 150 | resource = (resource && resource.scorm_type == 'sco') ? resource : nil 151 | return (resource && attribute) ? resource.send(attribute) : resource 152 | end 153 | end 154 | end -------------------------------------------------------------------------------- /lib/scorm/metadata.rb: -------------------------------------------------------------------------------- 1 | module Scorm 2 | 3 | # The +Metadata+ class holds meta data associated with a SCORM package in a 4 | # hash like structure. The +Metadata+ class reads a LOM (Learning Object 5 | # Metadata) structure and stores the data in categories. A +Category+ can 6 | # contain any number of +DataElement+s. A +DataElement+ behaves just like 7 | # a string but can contain the same value in many different languages, 8 | # accessed by the DataElement#value (or DataElement#to_s) method by 9 | # specifying the language code as the first argument. 10 | # 11 | # Ex. 12 | # 13 | # pkg.manifest.metadata.general.class -> Metadata::Category 14 | # pkg.manifest.metadata.general.title.class -> Metadata::DataElement 15 | # pkg.manifest.metadata.general.title.value -> 'My course' 16 | # pkg.manifest.metadata.general.title.value('sv') -> 'Min kurs' 17 | # 18 | class Metadata < Hash 19 | 20 | def self.from_xml(element) 21 | metadata = self.new 22 | element.elements.each do |category_el| 23 | category = Category.from_xml(category_el) 24 | metadata.store(category_el.name.to_s, category) 25 | end 26 | return metadata 27 | end 28 | 29 | def method_missing(sym) 30 | self.fetch(sym.to_s, nil) 31 | end 32 | 33 | class Category < Hash 34 | 35 | def self.from_xml(element) 36 | category = Scorm::Metadata::Category.new 37 | element.elements.each do |data_el| 38 | category[data_el.name.to_s] = DataElement.from_xml(data_el) 39 | end 40 | return category 41 | end 42 | 43 | def method_missing(sym, *args) 44 | data_element = self.fetch(sym.to_s, nil) 45 | if data_element.is_a? DataElement 46 | data_element.value(args.first) 47 | else 48 | data_element 49 | end 50 | end 51 | end 52 | 53 | class DataElement 54 | def initialize(value = '', default_lang = nil) 55 | if value.is_a? String 56 | @langstrings = Hash.new 57 | @langstrings['x-none'] = value 58 | @default_lang = 'x-none' 59 | elsif value.is_a? Hash 60 | @langstrings = value.dup 61 | @default_lang = default_lang || 'x-none' 62 | end 63 | end 64 | 65 | def self.from_xml(element) 66 | if element.elements.size == 0 67 | return self.new(element.text.to_s) 68 | 69 | elsif element.get_elements('value').size != 0 70 | value_el = element.get_elements('value').first 71 | return self.from_xml(value_el) 72 | 73 | elsif element.get_elements('langstring').size != 0 74 | langstrings = Hash.new 75 | default_lang = nil 76 | element.each_element('langstring') do |ls| 77 | default_lang = ls.attribute('xml:lang').to_s if default_lang.nil? 78 | langstrings[ls.attribute('xml:lang').to_s || 'x-none'] = ls.text.to_s 79 | end 80 | return self.new(langstrings, default_lang) 81 | 82 | else 83 | return Category.from_xml(element) 84 | 85 | end 86 | end 87 | 88 | def value(lang = nil) 89 | if lang.nil? 90 | (@langstrings && @default_lang) ? @langstrings[@default_lang] : '' 91 | else 92 | (@langstrings) ? @langstrings[lang] || '' : '' 93 | end 94 | end 95 | 96 | alias :to_s :value 97 | alias :to_str :value 98 | end 99 | end 100 | end -------------------------------------------------------------------------------- /lib/scorm/organization.rb: -------------------------------------------------------------------------------- 1 | #-- 2 | # TODO: items should be read as an hierarchy. 3 | # 4 | # TODO: imsss:sequencing and adlnav:presentation should be parsed and read. 5 | # 6 | # TODO: read .... 7 | #++ 8 | 9 | module Scorm 10 | # The +Organization+ class holds data about the organization of a SCORM 11 | # package. An organization contains an id, title and any number of +items+. 12 | # An +Item+ are (in most cases) the same thing as a SCO (Shareable Content 13 | # Object). 14 | class Organization 15 | attr_accessor :id 16 | attr_accessor :title 17 | attr_accessor :items 18 | 19 | def initialize(id, title, items) 20 | raise InvalidManifest, 'missing organization id' if id.nil? 21 | @id = id.to_s 22 | @title = title.to_s 23 | @items = items 24 | end 25 | 26 | def self.from_xml(element) 27 | id = element.attribute('identifier').to_s 28 | title = element.get_elements('title').first.text.to_s if element.get_elements('title').first 29 | items = [] 30 | REXML::XPath.each(element, 'item') do |item_el| 31 | items << Item.from_xml(item_el) 32 | end 33 | return self.new(id, title, items) 34 | end 35 | 36 | # An item has an id, title, and (in some cases) a parent item. An item is 37 | # associated with a resource, which in most cases is a SCO (Shareable 38 | # Content Object) resource. 39 | class Item 40 | attr_accessor :id 41 | attr_accessor :title 42 | attr_accessor :isvisible 43 | attr_accessor :parameters 44 | attr_accessor :resource_id 45 | attr_accessor :children 46 | attr_accessor :time_limit_action 47 | attr_accessor :data_from_lms 48 | attr_accessor :completion_threshold 49 | 50 | def initialize(id, title, isvisible = true, parameters = nil, resource_id = nil, children = nil) 51 | @id = id.to_s 52 | @title = title.to_s 53 | @isvisible = isvisible || true 54 | @parameters = parameters 55 | @resource_id = resource_id 56 | @children = children if children.is_a? Array 57 | end 58 | 59 | def self.from_xml(element) 60 | item_id = element.attribute('identifier').to_s 61 | item_title = element.get_elements('title').first.text.to_s if element.get_elements('title').first 62 | item_isvisible = (element.attribute('isvisible').to_s == 'true') 63 | item_parameters = element.attribute('parameters').to_s 64 | children = [] 65 | if element.get_elements('item').empty? 66 | resource_id = element.attribute('identifierref').to_s 67 | else 68 | element.each_element('item') do |item_el| 69 | child_item = self.from_xml(item_el) 70 | children << child_item 71 | end 72 | end 73 | return self.new(item_id, item_title, item_isvisible, item_parameters, resource_id, children) 74 | end 75 | end 76 | end 77 | end -------------------------------------------------------------------------------- /lib/scorm/package.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'zip/zip' 3 | require 'fileutils' 4 | require 'open-uri' 5 | require 'scorm/datatypes' 6 | require 'scorm/manifest' 7 | 8 | module Scorm 9 | class InvalidPackage < RuntimeError; end 10 | class InvalidManifest < InvalidPackage; end 11 | 12 | class Package 13 | attr_accessor :name # Name of the package. 14 | attr_accessor :manifest # An instance of +Scorm::Manifest+. 15 | attr_accessor :path # Path to the extracted course. 16 | attr_accessor :repository # The directory to which the packages is extracted. 17 | attr_accessor :options # The options hash supplied when opening the package. 18 | attr_accessor :package # The file name of the package file. 19 | 20 | DEFAULT_LOAD_OPTIONS = { 21 | :strict => false, 22 | :dry_run => false, 23 | :cleanup => true, 24 | :force_cleanup => false, 25 | :name => nil, 26 | :repository => nil 27 | } 28 | 29 | def self.set_default_load_options(options = {}) 30 | DEFAULT_LOAD_OPTIONS.merge!(options) 31 | end 32 | 33 | def self.open(filename, options = {}, &block) 34 | Package.new(filename, options, &block) 35 | end 36 | 37 | # This method will load a SCORM package and extract its content to the 38 | # directory specified by the +:repository+ option. The manifest file will be 39 | # parsed and made available through the +manifest+ instance variable. This 40 | # method should be called with an associated block as it yields the opened 41 | # package and then auto-magically closes it when the block has finished. It 42 | # will also do any necessary cleanup if an exception occur anywhere in the 43 | # block. The available options are: 44 | # 45 | # :+strict+: If +false+ the manifest will be parsed in a nicer way. Default: +true+. 46 | # :+dry_run+: If +true+ nothing will be written to the file system. Default: +false+. 47 | # :+cleanup+: If +false+ no cleanup will take place if an error occur. Default: +true+. 48 | # :+name+: The name to use when extracting the package to the 49 | # repository. Default: will use the filename of the package 50 | # (minus the .zip extension). 51 | # :+repository+: Path to the course repository. Default: the same directory as the package. 52 | # 53 | def initialize(filename, options = {}, &block) 54 | @options = DEFAULT_LOAD_OPTIONS.merge(options) 55 | @package = filename.respond_to?(:path) ? filename.path : filename 56 | 57 | # Check if package is a directory or a file. 58 | if File.directory?(@package) 59 | @name = File.basename(@package) 60 | @repository = File.dirname(@package) 61 | @path = File.expand_path(@package) 62 | else 63 | i = nil 64 | begin 65 | # Decide on a name for the package. 66 | @name = [(@options[:name] || File.basename(@package, File.extname(@package))), i].flatten.join 67 | 68 | # Set the path for the extracted package. 69 | @repository = @options[:repository] || File.dirname(@package) 70 | @path = File.expand_path(File.join(@repository, @name)) 71 | 72 | # First try is nil, subsequent tries sets and increments the value with 73 | # one starting at zero. 74 | i = (i || 0) + 1 75 | 76 | # Make sure the generated path is unique. 77 | end while File.exists?(@path) 78 | end 79 | 80 | # Extract the package 81 | extract! 82 | 83 | # Detect and read imsmanifest.xml 84 | if exists?('imsmanifest.xml') 85 | @manifest = Manifest.new(self, file('imsmanifest.xml')) 86 | else 87 | raise InvalidPackage, "#{File.basename(@package)}: no imsmanifest.xml, maybe not SCORM compatible?" 88 | end 89 | 90 | # Yield to the caller. 91 | yield(self) 92 | 93 | # Make sure the package is closed when the caller has finished reading it. 94 | self.close 95 | 96 | # If an exception occur the package is auto-magically closed and any 97 | # residual data deleted in a clean way. 98 | rescue Exception => e 99 | self.close 100 | self.cleanup 101 | raise e 102 | end 103 | 104 | # Closes the package. 105 | def close 106 | @zipfile.close if @zipfile 107 | 108 | # Make sure the extracted package is deleted if force_cleanup_on_close 109 | # is enabled. 110 | self.cleanup if @options[:force_cleanup_on_close] 111 | end 112 | 113 | # Cleans up by deleting all extracted files. Called when an error occurs. 114 | def cleanup 115 | FileUtils.rmtree(@path) if @options[:cleanup] && !@options[:dry_run] && @path && File.exists?(@path) && package? 116 | end 117 | 118 | # Extracts the content of the package to the course repository. This will be 119 | # done automatically when opening a package so this method will rarely be 120 | # used. If the +dry_run+ option was set to +true+ when the package was 121 | # opened nothing will happen. This behavior can be overridden with the 122 | # +force+ parameter. 123 | def extract!(force = false) 124 | return if @options[:dry_run] && !force 125 | 126 | # If opening an already extracted package; do nothing. 127 | if not package? 128 | return 129 | end 130 | 131 | # Create the path to the course 132 | FileUtils.mkdir_p(@path) 133 | 134 | Zip::ZipFile::foreach(@package) do |entry| 135 | entry_path = File.join(@path, entry.name) 136 | entry_dir = File.dirname(entry_path) 137 | FileUtils.mkdir_p(entry_dir) unless File.exists?(entry_dir) 138 | entry.extract(entry_path) 139 | end 140 | end 141 | 142 | # This will only return +true+ if what was opened was an actual zip file. 143 | # It returns +false+ if what was opened was a filesystem directory. 144 | def package? 145 | return false if File.directory?(@package) 146 | return true 147 | end 148 | 149 | # Reads a file from the package. If the file is not extracted yet (all files 150 | # are extracted by default when opening the package) it will be extracted 151 | # to the file system and its content returned. If the +dry_run+ option was 152 | # set to +true+ when opening the package the file will not be 153 | # extracted to the file system, but read directly into memory. 154 | def file(filename) 155 | if File.exists?(@path) 156 | File.read(path_to(filename)) 157 | else 158 | Zip::ZipFile.foreach(@package) do |entry| 159 | return entry.get_input_stream {|io| io.read } if entry.name == filename 160 | end 161 | end 162 | end 163 | 164 | # Returns +true+ if the specified file (or directory) exists in the package. 165 | def exists?(filename) 166 | if File.exists?(@path) 167 | File.exists?(path_to(filename)) 168 | else 169 | Zip::ZipFile::foreach(@package) do |entry| 170 | return true if entry.name == filename 171 | end 172 | false 173 | end 174 | end 175 | 176 | # Computes the absolute path to a file in an extracted package given its 177 | # relative path. The argument +relative+ can be used to get the path 178 | # relative to the course repository. 179 | # 180 | # Ex. 181 | # pkg.path => '/var/lms/courses/MyCourse/' 182 | # pkg.course_repository => '/var/lms/courses/' 183 | # path_to('images/myimg.jpg') => '/var/lms/courses/MyCourse/images/myimg.jpg' 184 | # path_to('images/myimg.jpg', true) => 'MyCourse/images/myimg.jpg' 185 | # 186 | def path_to(relative_filename, relative = false) 187 | if relative 188 | File.join(@name, relative_filename) 189 | else 190 | File.join(@path, relative_filename) 191 | end 192 | end 193 | 194 | # Returns an array with the paths to all the files in the package. 195 | def files 196 | if File.directory?(@package) 197 | Dir.glob(File.join(File.join(File.expand_path(@package), '**'), '*')).reject {|f| 198 | File.directory?(f) }.map {|f| f.sub(/^#{File.expand_path(@package)}\/?/, '') } 199 | else 200 | entries = [] 201 | Zip::ZipFile::foreach(@package) do |entry| 202 | entries << entry.name unless entry.name[-1..-1] == '/' 203 | end 204 | entries 205 | end 206 | end 207 | end 208 | end -------------------------------------------------------------------------------- /lib/scorm/resource.rb: -------------------------------------------------------------------------------- 1 | module Scorm 2 | 3 | # A +Resource+ is a representation/description of an actual resource (image, 4 | # sco, pdf, etc...) in a SCORM package. 5 | class Resource 6 | attr_accessor :id 7 | attr_accessor :type 8 | attr_accessor :scorm_type 9 | attr_accessor :href 10 | attr_accessor :metadata 11 | attr_accessor :files 12 | attr_accessor :dependencies 13 | 14 | def initialize(id, type, scorm_type, href = nil, metadata = nil, files = nil, dependencies = nil) 15 | raise InvalidManifest, 'Missing resource id' if id.nil? 16 | raise InvalidManifest, 'Missing resource type' if type.nil? 17 | breakpoint if scorm_type.nil? 18 | raise InvalidManifest, 'Missing resource scormType' if scorm_type.nil? 19 | @id = id.to_s 20 | @type = type.to_s 21 | @scorm_type = scorm_type.to_s 22 | @href = href.to_s || '' 23 | @metadata = metadata || Hash.new 24 | @files = files || [] 25 | @dependencies = dependencies || [] 26 | end 27 | 28 | def self.from_xml(element) 29 | metadata = nil 30 | files = [] 31 | REXML::XPath.each(element, 'file') do |file_el| 32 | files << element.attribute('xml:base').to_s + file_el.attribute('href').to_s 33 | end 34 | dependencies = [] 35 | REXML::XPath.each(element, 'dependency') do |dep_el| 36 | dependencies << dep_el.attribute('identifierref').to_s 37 | end 38 | 39 | res = self.new( 40 | element.attribute('identifier'), 41 | element.attribute('type'), 42 | element.attribute('scormType', 'adlcp') || element.attribute('scormtype', 'adlcp'), 43 | element.attribute('xml:base').to_s + element.attribute('href').to_s, 44 | metadata, 45 | files, 46 | dependencies) 47 | end 48 | end 49 | end -------------------------------------------------------------------------------- /scorm.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'scorm' 3 | s.version = '1.0.2' 4 | s.summary = 'Ruby library for reading, extracting and generating SCORM files.' 5 | s.description = 'SCORM is a Ruby library for reading and extracting Shareable Content Object Reference Model (SCORM) files. SCORM is a standardized package format used mainly by e-learning software to help with the exchange of course material between systems in an interoperable way. This gem supports SCORM 1.2 and SCORM 2004.' 6 | 7 | s.author = 'Niklas Holmgren' 8 | s.email = 'niklas.holmgren@mindset.se' 9 | s.homepage = 'http://github.com/mindset/scorm/' 10 | 11 | s.files = Dir['README', 'LICENSE', 'bin/**/*', 'examples/**/*', 'skeleton/**/*', 'lib/**/{*,.[a-z]*}'] 12 | s.require_path = 'lib' 13 | 14 | s.bindir = 'bin' 15 | s.executables = ['scorm'] 16 | s.default_executable = 'scorm' 17 | 18 | s.add_dependency('rubyzip', '~> 0.9.4') 19 | end -------------------------------------------------------------------------------- /skeleton/adlcp_rootv1p2.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /skeleton/ims_xml.xsd: -------------------------------------------------------------------------------- 1 | In namespace-aware XML processors, the "xml" prefix is bound to the namespace name http://www.w3.org/XML/1998/namespace. Do not reference this file in XML instances Schawn Thropp: Changed the uriReference type to string type Refers to universal XML 1.0 lang attribute Refers to XML Base: http://www.w3.org/TR/xmlbase -------------------------------------------------------------------------------- /skeleton/imscp_rootv1p1p2.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 19 | DRAFT XSD for IMS Content Packaging version 1.1 DRAFT 20 | Copyright (c) 2001 IMS GLC, Inc. 21 | 2000-04-21, Adjustments by T.D. Wason from CP 1.0. 22 | 2001-02-22, T.D.Wason: Modify for 2000-10-24 XML-Schema version. Modified to support extension. 23 | 2001-03-12, T.D.Wason: Change filename, target and meta-data namespaces and meta-data fielname. Add meta-data to itemType, fileType and organizationType. 24 | Do not define namespaces for xml in XML instances generated from this xsd. 25 | Imports IMS meta-data xsd, lower case element names. 26 | This XSD provides a reference to the IMS meta-data root element as imsmd:record 27 | If the IMS meta-data is to be used in the XML instance then the instance must define an IMS meta-data prefix with a namespace. The meta-data targetNamespace should be used. 28 | 2001-03-20, Thor Anderson: Remove manifestref, change resourceref back to identifierref, change manifest back to contained by manifest. --Tom Wason: manifest may contain _none_ or more manifests. 29 | 2001-04-13 Tom Wason: corrected attirbute name structure. Was misnamed type. 30 | 2001-05-14 Schawn Thropp: Made all complexType extensible with the group.any 31 | Added the anyAttribute to all complexTypes. Changed the href attribute on the fileType and resourceType to xsd:string 32 | Changed the maxLength of the href, identifierref, parameters, structure attributes to match the Information model. 33 | 2001-07-25 Schawn Thropp: Changed the namespace for the Schema of Schemas to the 5/2/2001 W3C XML Schema 34 | Recommendation. attributeGroup attr.imsmd deleted, was not used anywhere. Any attribute declarations that have 35 | use = "default" changed to use="optional" - attr.structure.req. 36 | Any attribute declarations that have value="somevalue" changed to default="somevalue", 37 | attr.structure.req (hierarchical). Removed references to IMS MD Version 1.1. 38 | Modified attribute group "attr.resourcetype.req" to change use from optional 39 | to required to match the information model. As a result the default value also needed to be removed 40 | Name change for XSD. Changed to match version of CP Spec 41 | 42 | 43 | 44 | Inclusions and Imports 45 | 46 | 47 | 48 | 49 | 50 | Attribute Declarations 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | element groups 158 | 159 | 160 | 161 | 162 | Any namespaced element from any namespace may be included within an "any" element. The namespace for the imported element must be defined in the instance, and the schema must be imported. 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | -------------------------------------------------------------------------------- /skeleton/imsmanifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ADL SCORM 5 | 1.2 6 | 7 | 8 | 9 | <langstring xml:lang="x-none">{{title}}</langstring> 10 | 11 | 12 | 13 | 14 | 1.0 15 | 16 | 17 | 18 | LOMv1.0 19 | 20 | 21 | Final 22 | 23 | 24 | 25 | 26 | ADL SCORM 1.2 27 | 28 | 29 | 30 | 31 | 32 | {{title}} 33 | 34 | {{title}} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /skeleton/imsmd_rootv1p2p1.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 2001-04-26 T.D.Wason. IMS meta-data 1.2 XML-Schema. 18 | 2001-06-07 S.E.Thropp. Changed the multiplicity on all elements to match the 19 | Final 1.2 Binding Specification. 20 | Changed all elements that use the langstringType to a multiplicy of 1 or more 21 | Changed centity in the contribute element to have a multiplicity of 0 or more. 22 | Changed the requirement element to have a multiplicity of 0 or more. 23 | 2001-07-25 Schawn Thropp. Updates to bring the XSD up to speed with the W3C 24 | XML Schema Recommendation. The following changes were made: Change the 25 | namespace to reference the 5/2/2001 W3C XML Schema Recommendation,the base 26 | type for the durtimeType, simpleType, was changed from timeDuration to duration. 27 | Any attribute declarations that have use="default" had to change to use="optional" 28 | - attr.type. Any attribute declarations that have value ="somevalue" had to change 29 | to default = "somevalue" - attr.type (URI) 30 | 2001-09-04 Schawn Thropp 31 | Changed the targetNamespace and namespace of schema to reflect version change 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Any namespaced element from any namespace may be used for an "any" element. The namespace for the imported element must be defined in the instance, and the schema must be imported. 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | --------------------------------------------------------------------------------