scorm-package 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ /*
2
+ Source code created by Rustici Software, LLC is licensed under a
3
+ Creative Commons Attribution 3.0 United States License
4
+ (http://creativecommons.org/licenses/by/3.0/us/)
5
+
6
+ Want to make SCORM easy? See our solutions at http://www.scorm.com.
7
+
8
+ This example provides for the bare minimum SCORM run-time calls.
9
+ It will demonstrate using the API discovery algorithm to find the
10
+ SCORM API and then calling Initialize and Terminate when the page
11
+ loads and unloads respectively. This example also demonstrates
12
+ some basic SCORM error handling.
13
+ */
14
+
15
+
16
+ //Include the standard ADL-supplied API discovery algorithm
17
+
18
+
19
+ ///////////////////////////////////////////
20
+ //Begin ADL-provided API discovery algorithm
21
+ ///////////////////////////////////////////
22
+ var findAPITries = 0;
23
+
24
+ function findAPI(win)
25
+ {
26
+ // Check to see if the window (win) contains the API
27
+ // if the window (win) does not contain the API and
28
+ // the window (win) has a parent window and the parent window
29
+ // is not the same as the window (win)
30
+ while ( (win.API == null) &&
31
+ (win.parent != null) &&
32
+ (win.parent != win) )
33
+ {
34
+ // increment the number of findAPITries
35
+ findAPITries++;
36
+
37
+ // Note: 7 is an arbitrary number, but should be more than sufficient
38
+ if (findAPITries > 7)
39
+ {
40
+ alert("Error finding API -- too deeply nested.");
41
+ return null;
42
+ }
43
+
44
+ // set the variable that represents the window being
45
+ // being searched to be the parent of the current window
46
+ // then search for the API again
47
+ win = win.parent;
48
+ }
49
+ return win.API;
50
+ }
51
+
52
+ function getAPI()
53
+ {
54
+ // start by looking for the API in the current window
55
+ var theAPI = findAPI(window);
56
+
57
+ // if the API is null (could not be found in the current window)
58
+ // and the current window has an opener window
59
+ if ( (theAPI == null) &&
60
+ (window.opener != null) &&
61
+ (typeof(window.opener) != "undefined") )
62
+ {
63
+ // try to find the API in the current window�s opener
64
+ theAPI = findAPI(window.opener);
65
+ }
66
+ // if the API has not been found
67
+ if (theAPI == null)
68
+ {
69
+ // Alert the user that the API Adapter could not be found
70
+ alert("Unable to find an API adapter");
71
+ }
72
+ return theAPI;
73
+ }
74
+
75
+
76
+ ///////////////////////////////////////////
77
+ //End ADL-provided API discovery algorithm
78
+ ///////////////////////////////////////////
79
+
80
+
81
+ //Create function handlers for the loading and unloading of the page
82
+
83
+
84
+ //Constants
85
+ var SCORM_TRUE = "true";
86
+ var SCORM_FALSE = "false";
87
+
88
+ //Since the Unload handler will be called twice, from both the onunload
89
+ //and onbeforeunload events, ensure that we only call LMSFinish once.
90
+ var finishCalled = false;
91
+
92
+ //Track whether or not we successfully initialized.
93
+ var initialized = false;
94
+
95
+ var API = null;
96
+
97
+ function ScormProcessInitialize(){
98
+ var result;
99
+
100
+ API = getAPI();
101
+
102
+ if (API == null){
103
+ alert("ERROR - Could not establish a connection with the LMS.\n\nYour results may not be recorded.");
104
+ return;
105
+ }
106
+
107
+ result = API.LMSInitialize("");
108
+
109
+ if (result == SCORM_FALSE){
110
+ var errorNumber = API.LMSGetLastError();
111
+ var errorString = API.LMSGetErrorString(errorNumber);
112
+ var diagnostic = API.LMSGetDiagnostic(errorNumber);
113
+
114
+ var errorDescription = "Number: " + errorNumber + "\nDescription: " + errorString + "\nDiagnostic: " + diagnostic;
115
+
116
+ alert("Error - Could not initialize communication with the LMS.\n\nYour results may not be recorded.\n\n" + errorDescription);
117
+ return;
118
+ }
119
+
120
+ initialized = true;
121
+ }
122
+
123
+ function ScormProcessFinish(){
124
+
125
+ var result;
126
+
127
+ //Don't terminate if we haven't initialized or if we've already terminated
128
+ if (initialized == false || finishCalled == true){return;}
129
+
130
+ result = API.LMSFinish("");
131
+
132
+ finishCalled = true;
133
+
134
+ if (result == SCORM_FALSE){
135
+ var errorNumber = API.LMSGetLastError();
136
+ var errorString = API.LMSGetErrorString(errorNumber);
137
+ var diagnostic = API.LMSGetDiagnostic(errorNumber);
138
+
139
+ var errorDescription = "Number: " + errorNumber + "\nDescription: " + errorString + "\nDiagnostic: " + diagnostic;
140
+
141
+ alert("Error - Could not terminate communication with the LMS.\n\nYour results may not be recorded.\n\n" + errorDescription);
142
+ return;
143
+ }
144
+ }
145
+
146
+
147
+ /*
148
+ Assign the processing functions to the page's load and unload
149
+ events. The onbeforeunload event is included because it can be
150
+ more reliable than the onunload event and we want to make sure
151
+ that Terminate is ALWAYS called.
152
+ */
153
+ window.onload = ScormProcessInitialize;
154
+ window.onunload = ScormProcessFinish;
155
+ window.onbeforeunload = ScormProcessFinish;
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zip"
4
+
5
+ module ScormPackage
6
+ module Packaging
7
+ class CreateZip
8
+ attr_reader :manifest, :lessons
9
+
10
+ root_path = File.expand_path("..", __dir__)
11
+
12
+ SCHEMA_FILES = [
13
+ File.join(root_path, "packaging", "common", "adlcp_rootv1p2.xsd"),
14
+ File.join(root_path, "packaging", "common", "ims_xml.xsd"),
15
+ File.join(root_path, "packaging", "common", "imscp_rootv1p1p2.xsd"),
16
+ File.join(root_path, "packaging", "common", "imsmd_rootv1p2p1.xsd"),
17
+ File.join(root_path, "packaging", "common", "scormfunctions.js")
18
+ ].freeze
19
+
20
+ def initialize(manifest, lessons)
21
+ @manifest = manifest
22
+ @lessons = lessons
23
+ end
24
+
25
+ def process
26
+ create_zip_package
27
+ end
28
+
29
+ private
30
+
31
+ def create_zip_package
32
+ buffer = Zip::OutputStream.write_buffer do |zipfile|
33
+ add_manifest(zipfile)
34
+ add_source_files(zipfile)
35
+ add_lesson_files(zipfile)
36
+ end
37
+ buffer.string
38
+ end
39
+
40
+ def add_manifest(zipfile)
41
+ zipfile.put_next_entry("imsmanifest.xml")
42
+ zipfile.write(manifest)
43
+ end
44
+
45
+ def add_source_files(zipfile)
46
+ SCHEMA_FILES.each do |file|
47
+ filename = File.basename(file)
48
+ zipfile.put_next_entry(filename)
49
+ zipfile.write(File.read(file))
50
+ end
51
+ end
52
+
53
+ def add_lesson_files(zipfile)
54
+ lessons.each do |lesson_path, lesson_content|
55
+ zipfile.put_next_entry(lesson_path)
56
+ zipfile.write(lesson_content)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "create_zip"
4
+
5
+ module ScormPackage
6
+ module Packaging
7
+ class Generate
8
+ attr_reader :course, :scorm_token
9
+
10
+ def initialize(course, scorm_token)
11
+ @course = course
12
+ @scorm_token = scorm_token
13
+ end
14
+
15
+ def process
16
+ manifest = generate_manifest
17
+ lessons_html = generate_lesson_contents
18
+ ScormPackage::Packaging::CreateZip.new(manifest, lessons_html).process
19
+ end
20
+
21
+ private
22
+
23
+ def generate_manifest
24
+ <<~XML
25
+ <?xml version="1.0" standalone="no" ?>
26
+ <manifest identifier="#{course.title}" version="1"
27
+ xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
28
+ xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_rootv1p2"
29
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
30
+ xsi:schemaLocation="http://www.imsproject.org/xsd/imscp_rootv1p1p2 imscp_rootv1p1p2.xsd
31
+ http://www.imsglobal.org/xsd/imsmd_rootv1p2p1 imsmd_rootv1p2p1.xsd
32
+ http://www.adlnet.org/xsd/adlcp_rootv1p2 adlcp_rootv1p2.xsd">
33
+ <metadata>
34
+ <schema>ADL SCORM</schema>
35
+ <schemaversion>1.2</schemaversion>
36
+ </metadata>
37
+ <organizations default="default-org">
38
+ <organization identifier="default-org">
39
+ <title>#{course.title}</title>
40
+ #{generate_modules(course.course_modules)}
41
+ </organization>
42
+ </organizations>
43
+ <resources>
44
+ #{generate_resources(course.course_modules)}
45
+ <resource identifier="common_files" type="webcontent" adlcp:scormtype="asset">
46
+ <file href="scormfunctions.js"/>
47
+ </resource>
48
+ </resources>
49
+ </manifest>
50
+ XML
51
+ end
52
+
53
+ def generate_modules(modules)
54
+ modules.map.with_index(1) do |mod, mod_index|
55
+ <<~XML
56
+ <item identifier="module-#{mod_index}">
57
+ <title>#{mod.title}</title>
58
+ #{generate_lessons(mod.lessons, mod_index)}
59
+ </item>
60
+ XML
61
+ end.join
62
+ end
63
+
64
+ def generate_lessons(lessons, mod_index)
65
+ lessons.map.with_index(1) do |lesson, lesson_index|
66
+ <<~XML
67
+ <item identifier="module-#{mod_index}-lesson-#{lesson_index}" identifierref="resource-module-#{mod_index}-lesson-#{lesson_index}">
68
+ <title>#{lesson.title}</title>
69
+ </item>
70
+ XML
71
+ end.join
72
+ end
73
+
74
+ def generate_resources(modules)
75
+ modules.flat_map.with_index(1) do |mod, mod_index|
76
+ mod.lessons.map.with_index(1) do |_, lesson_index|
77
+ <<~XML
78
+ <resource identifier="resource-module-#{mod_index}-lesson-#{lesson_index}" type="webcontent" adlcp:scormtype="sco" href="content/module#{mod_index}/lesson#{lesson_index}.html">
79
+ <file href="content/module#{mod_index}/lesson#{lesson_index}.html" />
80
+ <dependency identifierref="common_files" />
81
+ </resource>
82
+ XML
83
+ end
84
+ end.join
85
+ end
86
+
87
+ def generate_lesson_html(lesson)
88
+ <<~HTML
89
+ <!DOCTYPE html>
90
+ <html lang="en">
91
+ <head>
92
+ <meta charset="UTF-8">
93
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
94
+ <script src="../../scormfunctions.js"></script>
95
+ <title>#{lesson.title}</title>
96
+ <style>
97
+ .loader { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); }
98
+ iframe { display: none; width: 560px; height: 315px; border: none; }
99
+ </style>
100
+ </head>
101
+ <body>
102
+ <p>Lesson: #{lesson.title}</p>
103
+ <ul>#{lesson.videos.map do |video|
104
+ "<li>Language: #{video[:language]}<br>" \
105
+ "<div id=\"loader-#{video[:id]}\" class=\"loader\">Loading...</div>" \
106
+ "<iframe id=\"custom-iframe-#{video[:id]}\" data-video-url=\"#{video[:video_url]}\"></iframe></li>"
107
+ end.join}</ul>
108
+ <script>
109
+ const loadIframe = async (iframe) => {
110
+ const loader = document.getElementById(`loader-${iframe.id.split('-').pop()}`);
111
+ try {
112
+ loader.style.display = 'block';
113
+ const response = await fetch(iframe.dataset.videoUrl, {
114
+ headers: { 'X-Scorm-Token': '#{scorm_token}' }
115
+ });
116
+ iframe.srcdoc = await response.text();
117
+ iframe.style.display = 'block';
118
+ } catch (error) { console.error(error); }
119
+ finally { loader.style.display = 'none'; }
120
+ };
121
+ window.addEventListener('load', () =>
122
+ document.querySelectorAll('iframe[data-video-url]').forEach(loadIframe)
123
+ );
124
+ </script>
125
+ </body>
126
+ </html>
127
+ HTML
128
+ end
129
+
130
+ def generate_lesson_contents
131
+ lessons_hash = {}
132
+
133
+ course.course_modules.each_with_index do |mod, mod_index|
134
+ mod.lessons.each_with_index do |lesson, lesson_index|
135
+ lesson_path = "content/module#{mod_index + 1}/lesson#{lesson_index + 1}.html"
136
+ lessons_hash[lesson_path] = generate_lesson_html(lesson)
137
+ end
138
+ end
139
+ lessons_hash
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ScormPackage
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "scorm_package/version"
4
+ require_relative "scorm_package/abstract_course"
5
+ require_relative "scorm_package/abstract_course_module"
6
+ require_relative "scorm_package/abstract_lesson"
7
+ require_relative "scorm_package/packaging/generate"
8
+
9
+ module ScormPackage
10
+ class Error < StandardError; end
11
+ end
@@ -0,0 +1,4 @@
1
+ module ScormPackage
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scorm-package
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Sojan
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-01-29 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - paul.eliassojan@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rspec"
21
+ - ".rubocop.yml"
22
+ - LICENSE.txt
23
+ - README.md
24
+ - Rakefile
25
+ - lib/scorm_package.rb
26
+ - lib/scorm_package/abstract_course.rb
27
+ - lib/scorm_package/abstract_course_module.rb
28
+ - lib/scorm_package/abstract_lesson.rb
29
+ - lib/scorm_package/packaging/common/adlcp_rootv1p2.xsd
30
+ - lib/scorm_package/packaging/common/ims_xml.xsd
31
+ - lib/scorm_package/packaging/common/imscp_rootv1p1p2.xsd
32
+ - lib/scorm_package/packaging/common/imsmd_rootv1p2p1.xsd
33
+ - lib/scorm_package/packaging/common/scormfunctions.js
34
+ - lib/scorm_package/packaging/create_zip.rb
35
+ - lib/scorm_package/packaging/generate.rb
36
+ - lib/scorm_package/version.rb
37
+ - sig/scorm_package.rbs
38
+ homepage: https://github.com/openvitae-tech/scorm-package
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ homepage_uri: https://github.com/openvitae-tech/scorm-package
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 3.0.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.5.3
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Gem to create scorm package.
62
+ test_files: []