Prototype of offline/backendless search functionality and generating/building website from activity bundles (NOTE: cannot add incrementally new activity bundles for now as it will overwrite search index rather than appending to it)
This commit is contained in:
parent
d2e8a043fb
commit
fae90372df
@ -1,3 +1,2 @@
|
|||||||
# appstore
|
# Backendless search and website generator from activity bundles prototype
|
||||||
|
|
||||||
App store for Python 3 activities
|
|
106
generator/GeneralFunctions/DataStructureManupulations.py
Normal file
106
generator/GeneralFunctions/DataStructureManupulations.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
from sys import platform as OperatingSystem
|
||||||
|
|
||||||
|
Quotes = '"\''
|
||||||
|
|
||||||
|
if OperatingSystem == "win32":
|
||||||
|
PathSlash = '\\'
|
||||||
|
else:
|
||||||
|
PathSlash = '/'
|
||||||
|
FileProtocol = "file:" + 2*PathSlash
|
||||||
|
|
||||||
|
def ConvertToStandardPathFormat(Path):
|
||||||
|
""" Example,
|
||||||
|
Input: '"file:///some/path/somefile.extension"
|
||||||
|
Output: /some/path/somefile.extension
|
||||||
|
"""
|
||||||
|
Path = Path.strip(Quotes)
|
||||||
|
if Path.startswith(FileProtocol):
|
||||||
|
Path = Path[len(FileProtocol):]
|
||||||
|
return Path
|
||||||
|
|
||||||
|
def GetTextAfter(Text, ReadlinesTextFile):
|
||||||
|
for Lines in range(len(ReadlinesTextFile)):
|
||||||
|
Line = ReadlinesTextFile[Lines].strip('\n')
|
||||||
|
if Line.startswith(Text):
|
||||||
|
return Line[len(Text):]
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def SingleQuoteString(String):
|
||||||
|
if len(String) > 0:
|
||||||
|
if String[0] != '\'' or String[-1] != '\'':
|
||||||
|
String = '\'' + String + '\''
|
||||||
|
return String
|
||||||
|
|
||||||
|
def DoubleQuoteString(String):
|
||||||
|
if len(String) > 0:
|
||||||
|
if String[0] != '"' or String[-1] != '"':
|
||||||
|
String = '"' + String + '"'
|
||||||
|
return String
|
||||||
|
|
||||||
|
def ListIntoString(List, QuoteItems=0, Seprator=' '):
|
||||||
|
if QuoteItems == 2:
|
||||||
|
for i in range(len(List)):
|
||||||
|
Quoteditem = DoubleQuoteString(List[i])
|
||||||
|
List[i] = Quoteditem
|
||||||
|
elif QuoteItems == 1:
|
||||||
|
for i in range(len(List)):
|
||||||
|
Quoteditem = SingleQuoteString(List[i])
|
||||||
|
List[i] = Quoteditem
|
||||||
|
Stringoflist = (Seprator).join(List)
|
||||||
|
return Stringoflist
|
||||||
|
|
||||||
|
# strip=0 => remove both ' & ", 1 => remove ', 2 => remove "
|
||||||
|
def UnquoteString(String, strip=0):
|
||||||
|
while True:
|
||||||
|
if (
|
||||||
|
strip != 2
|
||||||
|
and String.startswith('"')
|
||||||
|
and String.endswith('"')):
|
||||||
|
String = String.strip('"')
|
||||||
|
elif (
|
||||||
|
strip != 1
|
||||||
|
and String.startswith("'")
|
||||||
|
and String.endswith("'")):
|
||||||
|
String = String.strip("'")
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return String
|
||||||
|
|
||||||
|
def StandardVariableName(Variable):
|
||||||
|
Variable = Variable.casefold()
|
||||||
|
Variable = Variable.replace('_', '').replace(' ', '')
|
||||||
|
return Variable
|
||||||
|
|
||||||
|
#def DictionaryToJsonStr(Dict, BaseIndentation=0):
|
||||||
|
#BI = '\t'*BaseIndentation
|
||||||
|
#JsonStr = BI+'{\n'
|
||||||
|
#for k, v in Dict.items():
|
||||||
|
#JsonStr += BI+'\t"'+k+'" : "'+v+'",\n'
|
||||||
|
#JsonStr = JsonStr[:-2]
|
||||||
|
#JsonStr += '\n'+BI+'}'
|
||||||
|
#return JsonStr
|
||||||
|
|
||||||
|
def StringToKeyValuePair(String, Seprator):
|
||||||
|
SepratorAt = String.find(Seprator)
|
||||||
|
if SepratorAt >= 0:
|
||||||
|
Key = String[:SepratorAt]
|
||||||
|
Value = String[SepratorAt+1:]
|
||||||
|
return Key, Value
|
||||||
|
else:
|
||||||
|
return "", String
|
||||||
|
|
||||||
|
def FormatStrForDictinary(String):
|
||||||
|
String = String.strip(" \n\r")
|
||||||
|
return UnquoteString(String)
|
||||||
|
|
||||||
|
def StrListToDictionary(
|
||||||
|
List,
|
||||||
|
Seprator = '=',
|
||||||
|
FormatFunction = FormatStrForDictinary):
|
||||||
|
Dictionary = {}
|
||||||
|
for i in List:
|
||||||
|
k, v = StringToKeyValuePair(i, Seprator)
|
||||||
|
k, v = FormatFunction(k), FormatFunction(v)
|
||||||
|
if len(k) > 0:
|
||||||
|
Dictionary[k] = v
|
||||||
|
return Dictionary
|
29
generator/GeneralFunctions/InputOutput.py
Normal file
29
generator/GeneralFunctions/InputOutput.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from os.path import isfile as DoesFileExist
|
||||||
|
|
||||||
|
from .DataStructureManupulations import ConvertToStandardPathFormat
|
||||||
|
|
||||||
|
def ReadTextFile(FilePath):
|
||||||
|
FilePath = ConvertToStandardPathFormat(FilePath)
|
||||||
|
if DoesFileExist(FilePath) is True:
|
||||||
|
File = open(FilePath)
|
||||||
|
ReadFile = File.read()
|
||||||
|
File.close()
|
||||||
|
return ReadFile
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def ReadlinesTextFile(FilePath):
|
||||||
|
String = ReadTextFile(FilePath)
|
||||||
|
return String.split('\n')
|
||||||
|
|
||||||
|
def WriteTextFiles(FilePath, Text):
|
||||||
|
if type(Text) != str:
|
||||||
|
Text = '\n'.join(Text)
|
||||||
|
File = open(FilePath, 'w')
|
||||||
|
File.write(Text)
|
||||||
|
File.close()
|
||||||
|
|
||||||
|
def WriteBinaryToFile(Filepath, Data):
|
||||||
|
File=open(Filepath, 'wb')
|
||||||
|
File.write(Data)
|
||||||
|
File.close()
|
8
generator/GeneralFunctions/Network.py
Normal file
8
generator/GeneralFunctions/Network.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import ssl
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
HttpsContext = ssl.create_default_context()
|
||||||
|
|
||||||
|
def Download(Url):
|
||||||
|
return urlopen(Url, context=HttpsContext).read()
|
||||||
|
|
16
generator/GeneralFunctions/OS.py
Normal file
16
generator/GeneralFunctions/OS.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
def CallFuncInDir(Directory, Function, *args, **kwArgs):
|
||||||
|
CurrentDir = os.getcwd()
|
||||||
|
os.chdir(Directory)
|
||||||
|
Function(*args, **kwArgs)
|
||||||
|
os.chdir(CurrentDir)
|
||||||
|
|
||||||
|
# return True if operation succesful and False if failed
|
||||||
|
def CreateDir(Directory):
|
||||||
|
if not os.path.isfile(Directory):
|
||||||
|
if not os.path.isdir(Directory):
|
||||||
|
os.mkdir(Directory)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
1
generator/GeneralFunctions/__init__.py
Normal file
1
generator/GeneralFunctions/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
288
generator/main.py
Normal file
288
generator/main.py
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
""" Run as:
|
||||||
|
python3 thisscript.py "/bundles/directoty" "/website/template/root/directory/
|
||||||
|
All sub-directories of bundles directory will be scanned for activity
|
||||||
|
bundles i.e. .xo files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
""" FIXME: paths hard coded unix style & most likely will not work on winodws.
|
||||||
|
Use code written for IMM to handle it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from urllib.parse import quote as strToHtmlFmt
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from GeneralFunctions.DataStructureManupulations import (
|
||||||
|
GetTextAfter,
|
||||||
|
UnquoteString,
|
||||||
|
StrListToDictionary
|
||||||
|
)
|
||||||
|
from GeneralFunctions.InputOutput import (
|
||||||
|
WriteTextFiles,
|
||||||
|
WriteBinaryToFile
|
||||||
|
)
|
||||||
|
from GeneralFunctions.OS import (
|
||||||
|
CallFuncInDir,
|
||||||
|
CreateDir
|
||||||
|
)
|
||||||
|
|
||||||
|
class extractData:
|
||||||
|
|
||||||
|
def findInfoFiles(self, bundle):
|
||||||
|
infoFiles = []
|
||||||
|
for File in bundle.namelist():
|
||||||
|
if File.endswith("activity/activity.info"):
|
||||||
|
infoFiles.append(File)
|
||||||
|
return infoFiles
|
||||||
|
|
||||||
|
def copyBundle(self, bundleSrc, activityName):
|
||||||
|
CallFuncInDir(
|
||||||
|
self.websiteDir,
|
||||||
|
shutil.copy2,
|
||||||
|
self.bundlesDir+bundleSrc,
|
||||||
|
"bundles/"+activityName+".xo"
|
||||||
|
)
|
||||||
|
|
||||||
|
def createDirectories(self):
|
||||||
|
assert(CreateDir("app"))
|
||||||
|
assert(CreateDir("icons"))
|
||||||
|
assert(CreateDir("bundles"))
|
||||||
|
assert(CreateDir("js"))
|
||||||
|
|
||||||
|
def extractActivityInfo(self, infoFilePath, zipFile):
|
||||||
|
infoList, infoDict = [], {}
|
||||||
|
infoList = zipFile.read(
|
||||||
|
infoFilePath).decode("utf-8").split('\n')
|
||||||
|
return StrListToDictionary(infoList)
|
||||||
|
|
||||||
|
def extractInfoAndIconFromBundles(self):
|
||||||
|
for bundlePath in self.activityBundles:
|
||||||
|
bundle = zipfile.ZipFile(bundlePath, "r")
|
||||||
|
|
||||||
|
infoFiles = self.findInfoFiles(bundle)
|
||||||
|
if len(infoFiles) != 1:
|
||||||
|
self.bundlesNotExactlyOneInfoFile.append(bundlePath)
|
||||||
|
else:
|
||||||
|
|
||||||
|
infoDict = self.extractActivityInfo(infoFiles[0], bundle)
|
||||||
|
self.bundlesInfoList.append(infoDict)
|
||||||
|
|
||||||
|
# FIXME: create seprate function for it
|
||||||
|
# extract and copy icon
|
||||||
|
activityName = infoDict.get("name")
|
||||||
|
if type(activityName) == str:
|
||||||
|
iconRelativePath = infoDict.get("icon")
|
||||||
|
if type(iconRelativePath) == str:
|
||||||
|
iconFolder = infoFiles[0][:infoFiles[0].rfind("/")+1]
|
||||||
|
iconAbsolutePath = (
|
||||||
|
iconFolder+iconRelativePath+".svg")
|
||||||
|
if iconAbsolutePath in bundle.namelist():
|
||||||
|
icon = bundle.read(iconAbsolutePath)
|
||||||
|
iconPath = (
|
||||||
|
"icons/"+
|
||||||
|
activityName
|
||||||
|
+".svg"
|
||||||
|
)
|
||||||
|
CallFuncInDir(
|
||||||
|
self.websiteDir,
|
||||||
|
WriteBinaryToFile,
|
||||||
|
iconPath,
|
||||||
|
icon
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Conitnue without icon since non-fatal error
|
||||||
|
self.iconErroredBundles.append(bundlePath)
|
||||||
|
|
||||||
|
bundle.close()
|
||||||
|
# FIXME: uncomment below function.
|
||||||
|
# Disabled sometime during devlopment as time consuming
|
||||||
|
self.copyBundle(bundlePath, activityName)
|
||||||
|
bundle.close()
|
||||||
|
|
||||||
|
def generateAppsHtmlPages(self):
|
||||||
|
iconsDir = "../icons/"
|
||||||
|
bundlesDir = "../bundles/"
|
||||||
|
for appInfo in self.indexDictList:
|
||||||
|
pathName = strToHtmlFmt(appInfo["name"], safe='')
|
||||||
|
|
||||||
|
html = (
|
||||||
|
'<!DOCTYPE html>\n<html>\n<head>\n<title>'+appInfo["name"]+
|
||||||
|
'</title>\n<meta charset="utf-8"/>\n<link rel="stylesheet" '
|
||||||
|
'type="text/css" href="../css/main.css"/>\n</head>\n<body>\n'
|
||||||
|
'</body>\n<h1>'+appInfo["name"]+'</h1>\n<p><img src="'+
|
||||||
|
str(iconsDir+pathName+'.svg')+'"></img></p>\n<div id=summary>'
|
||||||
|
'<h2>Summary</h2>\n<p>'+appInfo["summary"]+
|
||||||
|
'</p>\n</div>\n<div id=description><h2>Description</h2>\n<p>'+
|
||||||
|
appInfo["description"]+'</p>\n</div>\n<div id=tags><h2>Tags'
|
||||||
|
'</h2>\n<ul>\n'
|
||||||
|
)
|
||||||
|
for tag in appInfo["tags"]:
|
||||||
|
html += '<li>'+tag+'</li>\n'
|
||||||
|
html += (
|
||||||
|
'</ul>\n</div>\n<a href="'+str(bundlesDir+pathName+'.xo')+
|
||||||
|
'"><h2>Download<h2></a>\n</body>\n</html>'
|
||||||
|
)
|
||||||
|
|
||||||
|
WriteTextFiles("./app/"+appInfo["name"]+".html", html)
|
||||||
|
|
||||||
|
""" Only those which are specified in map will be added to index.
|
||||||
|
If an entry or value does not exist in infoJSON than emprty entry will
|
||||||
|
be created for it.
|
||||||
|
appends keys rather than replacing where mutiple map to same
|
||||||
|
"""
|
||||||
|
def generateIndex(self,
|
||||||
|
infoToIndexMap = {
|
||||||
|
"name" : ("name", str),
|
||||||
|
"summary" : ("summary", str),
|
||||||
|
"description" : ("description", str),
|
||||||
|
"tag" : ("tags", list),
|
||||||
|
"tags" : ("tags", list),
|
||||||
|
"categories" : ("tags", list),
|
||||||
|
"category" : ("tags", list)
|
||||||
|
}):
|
||||||
|
unexpectedInputError = (
|
||||||
|
"main.py generateIndex() : expect only str, list or tuple as "
|
||||||
|
"kwargs -> value[1] but found "
|
||||||
|
)
|
||||||
|
|
||||||
|
i2IMap = infoToIndexMap
|
||||||
|
self.indexDictList = []
|
||||||
|
|
||||||
|
for obj in json.loads(self.infoJson):
|
||||||
|
indexDict = {}
|
||||||
|
for k, v in obj.items():
|
||||||
|
if k in i2IMap:
|
||||||
|
|
||||||
|
# add new entry/key to app index
|
||||||
|
if k not in indexDict:
|
||||||
|
if i2IMap[k][1] == str:
|
||||||
|
indexDict[i2IMap[k][0]] = v
|
||||||
|
elif (i2IMap[k][1] == list
|
||||||
|
or i2IMap[k][1] == tuple):
|
||||||
|
if v.find(';') >= 0:
|
||||||
|
indexDict[i2IMap[k][0]] = v.split(';')
|
||||||
|
else:
|
||||||
|
indexDict[i2IMap[k][0]] = v.split()
|
||||||
|
|
||||||
|
# Append to existing entry/key to app index
|
||||||
|
else:
|
||||||
|
if i2IMap[k][1] == str:
|
||||||
|
indexDict[i2IMap[k][0]] += ' '+v
|
||||||
|
elif (i2IMap[k][1] == list
|
||||||
|
or i2IMap[k][1] == tuple):
|
||||||
|
if v.find(';') >= 0:
|
||||||
|
indexDict[i2IMap[k][0]] += v.split(';')
|
||||||
|
else:
|
||||||
|
indexDict[i2IMap[k][0]] += v.split()
|
||||||
|
else:
|
||||||
|
print(unexpectedInputError, i2IMap[k][1])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Create entry/key with empty value for keys not present
|
||||||
|
# in activty.info
|
||||||
|
for k ,v in i2IMap.items():
|
||||||
|
if v[0] not in indexDict:
|
||||||
|
if v[1] == str:
|
||||||
|
indexDict[v[0]] = ""
|
||||||
|
elif (v[1] == list or v[1] == tuple):
|
||||||
|
indexDict[v[0]] = ()
|
||||||
|
else:
|
||||||
|
print(unexpectedInputError, v[1])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
self.indexDictList.append(indexDict)
|
||||||
|
self.indexJs = "search.assignIndex("+json.dumps(self.indexDictList, indent=4)+")"
|
||||||
|
|
||||||
|
def generateInfoJson(self):
|
||||||
|
self.infoJson = json.dumps(self.bundlesInfoList)
|
||||||
|
|
||||||
|
def __init__(self, bundlesDir, websiteDir):
|
||||||
|
""" FIXME: WARNING:: some files may be missing such as some app
|
||||||
|
may not have icon (use a placeholder icon for them)
|
||||||
|
Some bundles are not succesfully processed (html page + bundle copy)
|
||||||
|
but are not in error logs as well. There are 495 bundles in
|
||||||
|
Tony's repo, 479 sucessfully processed but showing fatal error of
|
||||||
|
12 only, i.e. 4 missing.
|
||||||
|
"""
|
||||||
|
self.bundlesDir = bundlesDir
|
||||||
|
self.websiteDir = websiteDir
|
||||||
|
self.activityBundles = []
|
||||||
|
self.bundlesNotZipFiles = []
|
||||||
|
self.bundlesNotExactlyOneInfoFile = []
|
||||||
|
self.bundlesInfoList = []
|
||||||
|
self.infoJson = ''
|
||||||
|
self.indexDictList = []
|
||||||
|
self.erroredBundles = []
|
||||||
|
self.iconErroredBundles = []
|
||||||
|
|
||||||
|
CallFuncInDir(self.websiteDir, self.createDirectories)
|
||||||
|
|
||||||
|
os.chdir(bundlesDir)
|
||||||
|
self.activityBundles = glob.glob("**/*.xo", recursive=True)
|
||||||
|
|
||||||
|
self.purgeBundlesNotZipFile()
|
||||||
|
|
||||||
|
self.extractInfoAndIconFromBundles()
|
||||||
|
|
||||||
|
self.generateInfoJson()
|
||||||
|
|
||||||
|
self.generateIndex()
|
||||||
|
|
||||||
|
os.chdir(websiteDir)
|
||||||
|
|
||||||
|
self.generateAppsHtmlPages()
|
||||||
|
|
||||||
|
self.writeFiles()
|
||||||
|
|
||||||
|
#self.copyBundles()
|
||||||
|
|
||||||
|
def purgeBundlesNotZipFile(self):
|
||||||
|
activityBundles = []
|
||||||
|
for bundle in self.activityBundles:
|
||||||
|
if zipfile.is_zipfile(bundle):
|
||||||
|
activityBundles.append(bundle)
|
||||||
|
else:
|
||||||
|
self.bundlesNotZipFiles.append(bundle)
|
||||||
|
self.activityBundles = activityBundles
|
||||||
|
|
||||||
|
def writeFiles(self):
|
||||||
|
""" Files which are not continously written during the process
|
||||||
|
Eg. Html, icon and bundles are written while processing each bundle
|
||||||
|
"""
|
||||||
|
WriteTextFiles("info.json", self.infoJson)
|
||||||
|
WriteTextFiles("./js/index.js",self.indexJs)
|
||||||
|
WriteTextFiles(
|
||||||
|
"bundlesNotExactlyOneInfoFile", self.bundlesNotExactlyOneInfoFile)
|
||||||
|
WriteTextFiles("bundlesNotZipFiles.txt", self.bundlesNotZipFiles)
|
||||||
|
WriteTextFiles("erroredBundles.txt", self.erroredBundles)
|
||||||
|
WriteTextFiles("iconErroredBundles.txt", self.iconErroredBundles)
|
||||||
|
|
||||||
|
def processArguments():
|
||||||
|
variables = {}
|
||||||
|
if len(sys.argv) == 3:
|
||||||
|
variables["programDir"] = os.path.dirname(
|
||||||
|
os.path.realpath(sys.argv[0]))+'/'
|
||||||
|
variables["bundlesDir"] = os.path.realpath(sys.argv[1])+'/'
|
||||||
|
variables["websiteDir"] = os.path.realpath(sys.argv[2])+'/'
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"Please give exactly two arguments to program.\n"
|
||||||
|
"1. root directory of all activity bundles to be included "
|
||||||
|
"in website\n"
|
||||||
|
"2. root directory of website template\n"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
return variables
|
||||||
|
|
||||||
|
def main():
|
||||||
|
variables = processArguments()
|
||||||
|
extractData(variables["bundlesDir"], variables["websiteDir"])
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main();
|
199
website/css/main.css
Normal file
199
website/css/main.css
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Quicksand-Regular";
|
||||||
|
/* license: url("../fonts/Quicksand-Regular.txt"); */
|
||||||
|
src: url("../fonts/Quicksand-Regular.ttf");
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
/* background-color: #f4ffac4d; */
|
||||||
|
background-color: #fcfcfc;
|
||||||
|
color: #00386c;
|
||||||
|
font-family: Quicksand-Regular, roboto, sans-serif, monospace;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 900px;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 0.15rem solid #470598;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-bottom: 1rem
|
||||||
|
}
|
||||||
|
|
||||||
|
#Heading {
|
||||||
|
color: #099800; /*#00301fb3*/
|
||||||
|
font-size: 250%;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#Heading a {
|
||||||
|
color: #bfa800;
|
||||||
|
text-decoration: underline ;
|
||||||
|
}
|
||||||
|
|
||||||
|
#Heading a:hover {
|
||||||
|
color: #057e53 ;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
clear: both;
|
||||||
|
color: Crimson ;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: -0.25rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color1 {
|
||||||
|
color: #0c007e;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color1 a:hover {
|
||||||
|
color: DarkGoldenRod;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color2 {
|
||||||
|
color: Crimson;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color2 a:hover {
|
||||||
|
color: #9fbd06;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color3 {
|
||||||
|
color: ForestGreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color3 a:hover {
|
||||||
|
color: Teal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color4 {
|
||||||
|
color: DarkGoldenRod;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color4 a:hover {
|
||||||
|
color: #4f08a7;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color5 {
|
||||||
|
color: #90006b;
|
||||||
|
}
|
||||||
|
|
||||||
|
#color5 a:hover {
|
||||||
|
color: #08966e;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #0c007e;
|
||||||
|
font-size: 175%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: -0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
color: DarkGoldenRod;
|
||||||
|
font-size: 150%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: -0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
color: SeaGreen;
|
||||||
|
font-size: 125%;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: -0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
color: Teal;
|
||||||
|
font-size: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: -0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
color: ForestGreen ;
|
||||||
|
font-size: 100%;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#Date {
|
||||||
|
color: #0b8f73;
|
||||||
|
margin-bottom: -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #260000;
|
||||||
|
}
|
||||||
|
|
||||||
|
p a {
|
||||||
|
color: #90006b;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #057e53 ;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
background-color: Teal;
|
||||||
|
display: block; /*required for aligning image to center*/
|
||||||
|
margin: auto; /*align image to center*/
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
color: teal;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: 0.15rem solid #f60000;
|
||||||
|
font-size: 85%;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
color: #400090;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app_search {
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*=========================================*/
|
||||||
|
|
||||||
|
@media only screen and (min-width:10in) and (orientation:landscape) {
|
||||||
|
body {
|
||||||
|
/* margin: 0 5% 0 5%; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width:1000px) {
|
||||||
|
body {
|
||||||
|
margin: 0 2% 0 2%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (orientation:portrait) {
|
||||||
|
body {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
BIN
website/fonts/Quicksand-Regular.ttf
Normal file
BIN
website/fonts/Quicksand-Regular.ttf
Normal file
Binary file not shown.
93
website/fonts/Quicksand-Regular.txt
Normal file
93
website/fonts/Quicksand-Regular.txt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2011 The Quicksand Project Authors (https://github.com/andrew-paglinawan/QuicksandFamily), with Reserved Font Name “Quicksand”.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
1
website/index.html
Symbolic link
1
website/index.html
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
search.html
|
10872
website/js/jquery.js
vendored
Normal file
10872
website/js/jquery.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
160
website/js/search.js
Normal file
160
website/js/search.js
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
var similarString = function(a, b) {
|
||||||
|
return !a.localeCompare(b, undefined, {sensitivity: 'base'});
|
||||||
|
}
|
||||||
|
|
||||||
|
var countSimilarWordsInStrings = function(str1, str2) {
|
||||||
|
var count = 0;
|
||||||
|
var array1 = str1.split(' ');
|
||||||
|
var array2 = str2.split(' ');
|
||||||
|
for (var wordX of array1) {
|
||||||
|
for (var wordY of array2) {
|
||||||
|
if (similarString(wordX, wordY))
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: function not yet tested
|
||||||
|
var partialStrMatch = function(str1, str2) {
|
||||||
|
var lengthDiff = str1.length - str2.length;
|
||||||
|
|
||||||
|
if (lengthDiff < 0) {
|
||||||
|
[str1, str2] = [str2, str1];
|
||||||
|
lengthDiff = -lengthDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i=0; i <= lengthDiff; i++) {
|
||||||
|
var str1Part = str1.substr(i, i+lengthDiff);
|
||||||
|
if (similarString(str1Part, str2))
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var search = {
|
||||||
|
|
||||||
|
_index : null,
|
||||||
|
_queuedQuery : null,
|
||||||
|
|
||||||
|
appObjToHtml : function(app) {
|
||||||
|
var html = '<hr><h1><a href="./app/'+app.name+'.html">'+app.name+'</a></h1>\n<p><img src="./icons/'+app.name+'.svg" style="max-width: 250px"></img></p>\n<div id=summary><h2>Summary</h2>\n<p>'+app.summary+'</p>\n</div>\n<div id=description><h2>Description</h2>\n<p>'+app.description+'</p>\n</div>\n<div id=tags><h2>Tags</h2>\n<ul>\n';
|
||||||
|
for (var tag of app.tags)
|
||||||
|
html += '<li>'+ tag +'</li>\n';
|
||||||
|
html += '</ul>\n</div>\n';
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
|
||||||
|
assignIndex : function(index) {
|
||||||
|
this._index = index;
|
||||||
|
if (this._queuedQuery !== null) {
|
||||||
|
var queuedQuery = this._queuedQuery;
|
||||||
|
this._queuedQuery = null;
|
||||||
|
performSearch(queuedQuery);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
displayResults : function(results) {
|
||||||
|
$("#searchResults").show();
|
||||||
|
|
||||||
|
if (results[0][0] <= 0) {
|
||||||
|
$(".sR:eq(1)").html("<h1>No search result found</h1>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i=0; i <= $(".sR").length; i++) {
|
||||||
|
|
||||||
|
// No more entries matching query
|
||||||
|
if (results[i][0] <= 0)
|
||||||
|
break
|
||||||
|
|
||||||
|
var app = this._index[results[i][1]];
|
||||||
|
var html = this.appObjToHtml(app);
|
||||||
|
$(".sR:eq("+i+")").html(html);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getQueryParameters : function (queryString) {
|
||||||
|
var queryString = document.location.search;
|
||||||
|
return this.parseQueryParameters(queryString);
|
||||||
|
},
|
||||||
|
|
||||||
|
indexLoaded : function () {
|
||||||
|
return (this._index !== null);
|
||||||
|
},
|
||||||
|
|
||||||
|
init : function () {
|
||||||
|
var params = this.getQueryParameters();
|
||||||
|
if (params.q) {
|
||||||
|
var query = params.q[0];
|
||||||
|
query = query.replace("+", " ");
|
||||||
|
$('input[name="q"]')[0].value = query;
|
||||||
|
this.performSearch(query);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parseQueryParameters : function (queryString) {
|
||||||
|
var parameters = {}
|
||||||
|
var parts = queryString.substr(queryString.indexOf('?')+1).split('&');
|
||||||
|
for (var pair of parts) {
|
||||||
|
var spliter = pair.indexOf('=');
|
||||||
|
var key = pair.substr(0, spliter);
|
||||||
|
var value = pair.substr(spliter+1);
|
||||||
|
if (key in parameters)
|
||||||
|
parameters[key].push(value);
|
||||||
|
else
|
||||||
|
parameters[key] = [value];
|
||||||
|
}
|
||||||
|
return parameters;
|
||||||
|
},
|
||||||
|
|
||||||
|
performSearch : function (query) {
|
||||||
|
if (this.indexLoaded())
|
||||||
|
this.processSearch(query);
|
||||||
|
else
|
||||||
|
this._queuedQuery = query;
|
||||||
|
},
|
||||||
|
|
||||||
|
processSearch : function (query) {
|
||||||
|
var results = this.rankApps(query);
|
||||||
|
|
||||||
|
results.sort(function(a, b) {
|
||||||
|
// sort in descending rank order
|
||||||
|
return b[0]-a[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
this.displayResults(results);
|
||||||
|
},
|
||||||
|
|
||||||
|
rankApp : function(app, query) {
|
||||||
|
var rank = 0;
|
||||||
|
rank += 10*countSimilarWordsInStrings(app.name, query);
|
||||||
|
// rank += partialStrMatch(app.name, query);
|
||||||
|
rank += 3*countSimilarWordsInStrings(app.summary, query);
|
||||||
|
// rank += partialStrMatch(app.summary, query);
|
||||||
|
rank += 2*countSimilarWordsInStrings(app.description, query);
|
||||||
|
// rank += partialStrMatch(app.description, query);
|
||||||
|
for (var tag of app.tags)
|
||||||
|
rank += 5*countSimilarWordsInStrings(tag, query);
|
||||||
|
// below statement causes error: tag is undefined when empty?
|
||||||
|
// rank += partialStrMatch(tag, query);
|
||||||
|
return rank;
|
||||||
|
},
|
||||||
|
|
||||||
|
rankApps : function(query) {
|
||||||
|
var results = [];
|
||||||
|
for (var i=0; i < this._index.length; i++) {
|
||||||
|
var app = this._index[i];
|
||||||
|
var rank = this.rankApp(app, query);
|
||||||
|
results.push([rank, i]);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
$(document).ready( function() {
|
||||||
|
search.init();
|
||||||
|
});
|
75
website/search.html
Normal file
75
website/search.html
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Sugar Activites App Store Search Prototype</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="./css/main.css"/>
|
||||||
|
<script type="application/javascript" src="./js/jquery.js"></script>
|
||||||
|
<script type="application/javascript" src="./js/search.js"></script>
|
||||||
|
<script type="application/javascript" src="./js/index.js" defer></script>
|
||||||
|
<!-- <script type="application/json" src="./index.json" defer></script> -->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id=searchBar>
|
||||||
|
<form action="" method="GET" id=app_search>
|
||||||
|
<input
|
||||||
|
type=text
|
||||||
|
placeholder="Search Activities"
|
||||||
|
name="q"
|
||||||
|
id="search_box"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Search"
|
||||||
|
id="search_button"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id=searchResults style="display:none">
|
||||||
|
<p class=sR>
|
||||||
|
<div class=appName>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p class=sR>
|
||||||
|
<div class=appName>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p class=sR>
|
||||||
|
<div class=appName>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p class=sR>
|
||||||
|
<div class=appName>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p class=sR>
|
||||||
|
<div class=appName>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p class=sR>
|
||||||
|
<div class=appName>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p class=sR>
|
||||||
|
<div class=appName>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p class=sR>
|
||||||
|
<div class=appName>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p class=sR>
|
||||||
|
<div class=appName>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<p class=sR>
|
||||||
|
<div class=appName>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user