├── .gitattributes ├── .gitignore ├── Dockerfile ├── Dockerfile-webhook ├── Instructions.md ├── README.md ├── build.gradle ├── css ├── tooltipster-shadow.css └── tooltipster.css ├── docker-compose.yml ├── docs └── privacy.html ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── embed_logo.png ├── embed_logo.svg ├── favicons │ ├── android-chrome-192x192.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── manifest.json │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ └── safari-pinned-tab.svg ├── logo.svg └── logo_docs.svg ├── js ├── curse-ads.js ├── docs.js ├── files.js ├── google-ads.js ├── sidebar.js ├── theme-switch-toggle.js └── theme-switch.js ├── python ├── generators.py ├── mc_version.py ├── metadata.py ├── page_generator.py ├── page_generator_service.py └── templates.py ├── requirements.txt ├── runTestPageGen.bat ├── runTestPageGen.sh ├── sass ├── _ads.scss ├── _base.scss ├── _docs.scss ├── _downloads.scss ├── _forum_screens.scss ├── _forums.scss ├── _highlighting_dark.scss ├── _highlighting_light.scss ├── _layout.scss ├── _layout_screens.scss ├── _normalize.scss ├── _privacy.scss ├── _reboot.scss ├── _scrollpane.scss ├── _sidebars.scss ├── _styles.scss ├── _styles_dark.scss ├── _styles_light.scss ├── _theme_dark.scss ├── _theme_light.scss ├── documentation_dark.scss ├── documentation_light.scss ├── forums_dark.scss ├── forums_light.scss ├── website_dark.scss └── website_light.scss ├── settings.gradle └── templates ├── base_page.html ├── page_body.html ├── page_directory_body.html ├── page_footer.html ├── page_header.html └── project_index.html /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.bat text eol=crlf 3 | *.patch text eol=lf 4 | *.java text eol=lf 5 | *.gradle text eol=crlf 6 | *.png binary 7 | *.gif binary 8 | *.exe binary 9 | *.dll binary 10 | *.jar binary 11 | *.lzma binary 12 | *.zip binary 13 | *.pyd binary 14 | *.cfg text eol=lf 15 | *.py text eol=lf 16 | *.jks binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__/ 2 | /output_web/ 3 | *.bat 4 | /cache/ 5 | /output_meta/ 6 | /.venv/ 7 | /.idea/ 8 | /build 9 | /.gradle 10 | /tstin/ 11 | /tstout/ 12 | /out/ 13 | /python/__pycache__/ 14 | /test.sh 15 | /test/* 16 | !/test/Help.txt 17 | /repo/ 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-alpine 2 | 3 | VOLUME /in 4 | VOLUME /out 5 | 6 | WORKDIR /app 7 | 8 | COPY requirements.txt ./ 9 | RUN pip install --no-cache-dir -r requirements.txt 10 | 11 | COPY python python 12 | COPY templates templates 13 | 14 | COPY build/distributions/static-bundle.zip . 15 | RUN unzip static-bundle.zip -d static && rm static-bundle.zip 16 | 17 | ENTRYPOINT [ "python", "-u", "python/page_generator.py" ] 18 | -------------------------------------------------------------------------------- /Dockerfile-webhook: -------------------------------------------------------------------------------- 1 | FROM python:3.9-alpine 2 | 3 | VOLUME /in 4 | VOLUME /out 5 | 6 | WORKDIR /app 7 | 8 | COPY requirements.txt ./ 9 | RUN pip install --no-cache-dir -r requirements.txt 10 | 11 | COPY python python 12 | COPY templates templates 13 | 14 | COPY build/distributions/static-bundle.zip . 15 | RUN unzip static-bundle.zip -d static && rm static-bundle.zip 16 | 17 | ENTRYPOINT [ "python", "-u", "python/page_generator_service.py" ] 18 | -------------------------------------------------------------------------------- /Instructions.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | There's two main parts to the build system and testing procedure: 4 | 1. Python pagegen for generated content (html templates, files in the `./test/out` folder) 5 | 2. Gradle for static content (js, sass -> css and images) 6 | 7 | ## Folder structure 8 | 9 | Here's a brief description on what each of the folders are for that we understand: 10 | 11 | - `gradle` 12 | - contains the gradle wrapper jar 13 | - `images` 14 | - contains png and svg images 15 | - `js` 16 | - contains JavaScript files that are put on various places of Forge's websites 17 | - `python` 18 | - the PageGen code 19 | - `test` 20 | - this is where you can test your changes 21 | - `maven` 22 | - contains dummy data used by pagegen, generated by the `setupTest` Gradle task 23 | - you can make your own by running the `publish` command on most Forge projects and copy the literal folder called `repo` from there over to here 24 | - poms and the like don't seem to be used by the pagegen, so feel free to make folders, add files inside and change up the json as you see fit to aid testing. 25 | - As long as it follows the expected format similar to the dummy data, it should work 26 | - `out` 27 | - the generated output by pagegen 28 | - `static` 29 | - contains compiled static content, such as the css, js and images, made by the `setupTest` 30 | - `sass` 31 | - poorly named, actually has scss files inside which are used for styling 32 | - they get compiled to css by the gradle part of the build system 33 | - `static` 34 | - contains compiled static content, such as the css, js and images 35 | - during testing, you probably want to compile to this folder and tell pagegen to use this with the `--static` arg 36 | - this folder also contains a copy of the images folder 37 | - `templates` 38 | - the HTML templates used by the Python pagegen side of the build system 39 | 40 | # Instructions 41 | 42 | Here's some instructions on how to setup the environment and work with it. 43 | 44 | ## Step 1: Setting up the test environment 45 | 46 | Firstly, we need to setup the Python side: 47 | 1. Make sure Python is installed. At the time of writing, the latest Python 3.10.4 seems to work 48 | 49 | Now, let's setup the Gradle side: 50 | 1. Import the `build.gradle` into your IDE 51 | 2. Run `gradlew setupTest` 52 | 53 | ## Step 2: Run PageGen 54 | 55 | 1. Run `gradlew runTestPageGen` 56 | 2. Open the pages you want to test, they can be found in the `./test/out` folder 57 | 58 | ## Step 3: Making changes 59 | 60 | For HTML: 61 | 1. Edit the appropriate file(s) in the `./templates` folder 62 | 2. Run `gradlew runTestPageGen` 63 | 3. Open the pages you want to test in the `./test/out` folder 64 | 65 | For CSS: 66 | 1. Edit the appropriate file(s) in the `./css` folder 67 | 2. Run `gradlew setupTest` 68 | 3. Run `gradlew runTestPageGen` 69 | 4. Open the pages you want to test in the `./test/out` folder 70 | 71 | For SCSS: 72 | 1. Edit the appropriate file(s) in the `./sass` folder 73 | 2. Run `gradlew setupTest` 74 | 3. Run `gradlew runTestPageGen` 75 | 4. Open the pages you want to test in the `./test/out` folder 76 | 77 | For JS: 78 | 1. Edit the appropriate file(s) in the `./js` folder 79 | 2. Run `gradlew setupTest` 80 | 3. Run `gradlew runTestPageGen` 81 | 4. Open the pages you want to test in the `./test/out` folder 82 | 83 | For images: 84 | 1. Edit the appropriate file(s) in the `./images` folder 85 | 2. Run `gradlew setupTest` 86 | 3. Run `gradlew runTestPageGen` 87 | 4. Open the pages you want to test in the `./test/out` folder 88 | 89 | For the Python side of the build system: 90 | 1. Edit the appropriate file(s) in the `./python` folder and the `requirements.txt` 91 | 2. Run `setupPageGen` if you changed the `requirements.txt` 92 | 3. Test PageGen with `runTestPageGen` and opening the pages you want to test in the `./test/out` folder 93 | 94 | For the Gradle side of the build system: 95 | 1. Edit the appropriate Gradle file(s), such as `build.gradle`, `gradlew`, `gradlew.bat` and the `./gradle` folder 96 | 2. Refresh Gradle as usual 97 | 98 | ## Step 4: Committing 99 | 100 | In your PR, commit any changed files inside the `./css`, `./sass`, `./js`, `./templates`, `./images` and `./python` folders 101 | 102 | Note: The test html files generated in Step 2 are not intended to be published to a live site, as they contain `file://` references to your machine for some static resources, which won't work for anyone else. 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Forge Web 2 | ========= 3 | 4 | This repository serves as central hub for all things web and [Forge](https://github.com/MinecraftForge/MinecraftForge/). This includes stylesheets, images and scripts for Forge sites such as the docs and the [forums](https://minecraftforge.net/). 5 | 6 | SASS Structure 7 | -------------- 8 | To ensure that the look and feel across all sites is consistent, we heavily rely on the [SASS preprocessor](http://sass-lang.com/) to modularize and automatically generate our stylesheets. 9 | 10 | Hence, you will find a specific structure when looking at the stylesheets: 11 | 12 | - Only files without an underscore prefix will get deployed to the sites and are the place where all other components come together. These files should stay as they are right now unless new modules are added or old ones removed. 13 | - Files with an underscore make up discrete modules of the stylesheets, their name specifies each module's purpose. 14 | - Files with a 'theme' prefix serve as mere variable references to allow for flexible and easy switching of color themes. 15 | - Files with a 'styles' prefix serve as combinations of multiple modules along with the respective themes, while the simple 'styles.scss' file is the location where generic styles are brought together. 16 | - Files with a 'screens' suffix generally contain nothing but media queries that specify adjustments for different screen and device sizes. 17 | 18 | Contribution Guidelines 19 | ----------------------- 20 | You can find instructions on how to setup, develop and make contributions to this repo [here](Instructions.md). 21 | 22 | Contributions such as fixes or additions to the stylesheets are welcome, but they should adhere to the following rules: 23 | 24 | - The sites make heavy use of the flexbox model. Its capabilities should be used to the fullest rather than relying on hacks simply to work in old browsers. 25 | - Colors mustn't be hardcoded unless they work well across all themes. Either stick to the current color palette and use the theme variables or, if no other way is possible, introduce a new theme variable. 26 | - All dimensional measurements must be specified in `rem` or as a percentage. This ensures a consistent look and will make potential font sizes changes less of a hassle. 27 | - Unless absolutely necessary, new modules or edits to the deployment files should be avoided. Changes should go into an appropriate file, but one shouldn't need to look for a single change in too many files. 28 | - Use of all capabilities of SASS is helpful. Try to nest things where a hierarchy is given and comment the topmost element accordingly. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'maven-publish' 3 | id "io.freefair.jsass-base" version "1.0.2" 4 | id "com.magnetichq.gradle.js" version "3.0.2" 5 | id 'net.minecraftforge.gradleutils' version '[2.3,2.4)' 6 | } 7 | 8 | group = 'net.minecraftforge' 9 | version = gradleutils.tagOffsetVersion 10 | println "Version: $version" 11 | 12 | tasks.register("css", io.freefair.gradle.plugins.jsass.SassCompile) { 13 | sassPath = "${projectDir}/sass" 14 | cssPath = "${buildDir}/css" 15 | sourceMapEnabled = false 16 | //outputStyle = io.bit3.jsass.OutputStyle.NESTED 17 | outputStyle = io.bit3.jsass.OutputStyle.COMPRESSED 18 | } 19 | def minifyJsFiles = tasks.register("minifyJsFiles") 20 | void createJsTask(final String taskName, final String fileName) { 21 | def tmp = tasks.register("minify${taskName.capitalize()}DotJs", com.eriwen.gradle.js.tasks.MinifyJsTask) { 22 | source = file("${projectDir}/js/${fileName}.js") 23 | dest = file("${buildDir}/js/${fileName}.min.js") 24 | } 25 | minifyJsFiles.dependsOn(tmp) 26 | } 27 | createJsTask('themeSwitch', "theme-switch") 28 | createJsTask('themeSwitchToggle', "theme-switch-toggle") 29 | createJsTask('curseAds', "curse-ads") 30 | createJsTask('googleAds', "google-ads") 31 | createJsTask('sidebar', "sidebar") 32 | createJsTask('files', "files") 33 | tasks.register('bundleStatic', Zip) { 34 | dependsOn minifyJsFiles 35 | archiveBaseName = 'static-bundle' 36 | destinationDirectory = file("${buildDir}/distributions") 37 | from(file("${buildDir}/js")) { 38 | include '*.js' 39 | into 'js' 40 | } 41 | from(css) { 42 | include 'website_*.css' 43 | into 'css' 44 | rename ~/website_([A-Za-z]+)/, 'styles_$1' 45 | } 46 | from("${projectDir}/css") { 47 | include '*.css' 48 | into 'css' 49 | } 50 | from("${projectDir}/images") { 51 | include '**' 52 | into 'images' 53 | } 54 | from("${projectDir}/docs") { 55 | include '**/*.html' 56 | } 57 | } 58 | tasks.register('bundleFiles', Zip) { 59 | dependsOn bundleStatic 60 | archiveBaseName = 'full-bundle' 61 | destinationDirectory = file("${buildDir}/distributions") 62 | from(zipTree(bundleStatic.archiveFile)) { 63 | into 'static' 64 | } 65 | from(file("${projectDir}/python")) { 66 | exclude '__pycache__/' 67 | into 'python' 68 | } 69 | from(file("${projectDir}/templates")) { 70 | into 'templates' 71 | } 72 | from(file("${projectDir}/requirements.txt")) 73 | } 74 | 75 | @groovy.transform.CompileStatic 76 | static String digest(String data, String hash) { 77 | return java.security.MessageDigest.getInstance(hash).digest(data.bytes).encodeHex().toString() 78 | } 79 | void writeArtifact(group, name, version, classifier = null, String ext = 'jar') { 80 | String path = "${group}/${name}/${version}/${name}-${version}" 81 | if (classifier != null) 82 | path += '-' + classifier 83 | path += '.' + ext 84 | file("./test/maven/${path}").parentFile.mkdirs() 85 | file("./test/maven/${path}").text = path 86 | file("./test/maven/${path}.md5").text = digest(path, 'MD5') 87 | file("./test/maven/${path}.sha1").text = digest(path, 'SHA-1') 88 | file("./test/maven/${path}.sha256").text = digest(path, 'SHA-256') 89 | } 90 | void writeXml(String group, String name, String displayName, List vers, boolean writePromos) { 91 | final writer = new StringWriter() 92 | def xml = new groovy.xml.MarkupBuilder(writer) 93 | String path = "./test/maven/${group}/${name}" 94 | 95 | xml.metadata() { 96 | groupId(group.replace('/', '.')) 97 | artifactId(name) 98 | versioning() { 99 | release(vers[-1]) 100 | latest(vers[-1]) 101 | lastUpdated(20220101010000) 102 | versions() { 103 | vers.each { ver -> xml.version(ver) } 104 | } 105 | } 106 | } 107 | String data = writer.toString() 108 | file("${path}/").mkdirs() 109 | file("${path}/maven-metadata.xml").text = data 110 | file("${path}/maven-metadata.xml.md5").text = digest(data, 'MD5') 111 | file("${path}/maven-metadata.xml.sha1").text = digest(data, 'SHA-1') 112 | file("${path}/maven-metadata.xml.sha256").text = digest(data, 'SHA-256') 113 | file("${path}/page_config.json").text = new groovy.json.JsonBuilder([ 114 | name: displayName, 115 | comments: false, 116 | downloads: true, 117 | shrink_dls: false, 118 | adfocus: "271228" 119 | ]).toPrettyString() 120 | if (writePromos) { 121 | file("${path}/promotions_slim.json").text = new groovy.json.JsonBuilder([ 122 | homepage: 'https://not_real.invalid/', 123 | promos: [ 124 | '1.16.5-latest': '36.1.2', 125 | '1.16.5-recommended': '36.1.2', 126 | '1.18.1-latest': '39.1.2', 127 | '1.18.1-recommended': '39.1.2', 128 | '1.18.2-latest': '40.0.8' 129 | ] 130 | ]).toPrettyString() 131 | } 132 | } 133 | tasks.register("deleteTestData", Delete) { 134 | delete './test/' 135 | } 136 | tasks.register('generateMaven') { 137 | mustRunAfter deleteTestData 138 | doLast { 139 | List versions = [] 140 | ['1.16.5-36.0', '1.16.5-36.1', '1.18.1-39.0', '1.18.1-39.1', '1.18.2-40.0', '1.18.2-40.1'].each { ver -> 141 | def range = 0..8 // make eight builds for "beta" release period 142 | if (ver.endsWith('.1')) 143 | range = 0..2 // make three builds for "recommended" release period 144 | 145 | range.each { build -> 146 | versions.add("${ver}.${build}") 147 | writeArtifact('net/minecraftforge', 'forge', "${ver}.${build}") 148 | writeArtifact('net/minecraftforge', 'forge', "${ver}.${build}", 'userdev') 149 | writeArtifact('net/minecraftforge', 'forge', "${ver}.${build}", 'changelog', 'txt') 150 | writeArtifact('net/minecraftforge', 'forge', "${ver}.${build}", 'mdk', 'zip') 151 | writeArtifact('net/minecraftforge', 'forge', "${ver}.${build}", 'installer') 152 | } 153 | } 154 | writeXml('net/minecraftforge', 'forge', "Minecraft Forge", versions, true) 155 | 156 | versions = [] 157 | ['23w13a_or_b-april.2023.13.b', '23w17a-2023.17.a', '1.20.1-pre1-46.0', '1.19.4-45.0', '1.20-rc1-46.1'].each { ver -> 158 | def range = 0..8 // make eight builds 159 | 160 | range.each { build -> 161 | versions.add("${ver}.${build}") 162 | writeArtifact('net/minecraftforge', 'froge', "${ver}.${build}") 163 | writeArtifact('net/minecraftforge', 'froge', "${ver}.${build}", 'userdev') 164 | writeArtifact('net/minecraftforge', 'froge', "${ver}.${build}", 'changelog', 'txt') 165 | writeArtifact('net/minecraftforge', 'froge', "${ver}.${build}", 'mdk', 'zip') 166 | writeArtifact('net/minecraftforge', 'froge', "${ver}.${build}", 'installer') 167 | } 168 | } 169 | writeXml('net/minecraftforge', 'froge', "Minecraft Froge", versions, false) 170 | } 171 | } 172 | tasks.register('generateGlobalConfig') { 173 | mustRunAfter deleteTestData 174 | doLast { 175 | def file = file('./test/config/global_overrides.json') 176 | file.parentFile.mkdirs() 177 | file.text = new groovy.json.JsonBuilder([ 178 | ad_left: "
", 179 | ad_middle: "
", 180 | ad_right: "
" 181 | ]).toPrettyString() 182 | } 183 | } 184 | tasks.register('generateTestUsers') { 185 | mustRunAfter deleteTestData 186 | doLast { 187 | def file = file('./test/config/users.json') 188 | file.parentFile.mkdirs() 189 | file.text = new groovy.json.JsonBuilder([ 190 | test_name: [ 191 | password: 'test_password', 192 | access: [ 193 | [group: '', artifact: ''] 194 | ] 195 | ] 196 | ]).toPrettyString() 197 | } 198 | } 199 | tasks.register('generateTestConfig') { 200 | dependsOn generateGlobalConfig, generateTestUsers 201 | mustRunAfter deleteTestData 202 | doLast { 203 | def cfg = file('./test/config/config.json') 204 | cfg.parentFile.mkdirs() 205 | cfg.text = new groovy.json.JsonBuilder([ 206 | promote: [ 207 | args: [ '--folder', '/in', '--local-data', '--config', '/config/global_overrides.json', '--static', 'file://' + file('./test/static/').absolutePath] 208 | ] 209 | ]).toPrettyString() 210 | } 211 | } 212 | tasks.register('extractBundledFiles', Copy) { 213 | dependsOn bundleStatic 214 | mustRunAfter deleteTestData 215 | from zipTree(bundleStatic.archiveFile) 216 | into file('./test/static/') 217 | } 218 | tasks.register('copyTestTemplates', Copy) { 219 | mustRunAfter deleteTestData 220 | from file('./templates/') 221 | into file('./test/templates/') 222 | } 223 | tasks.register('copyTestPython', Copy) { 224 | mustRunAfter deleteTestData 225 | from file('./python/') 226 | into file('./test/python/') 227 | } 228 | tasks.register('runTestPageGen', Exec) { 229 | dependsOn = [copyTestPython, copyTestTemplates] 230 | workingDir 'test' 231 | String os = System.getProperty('os.name').toLowerCase(Locale.ROOT) 232 | if (os.contains('win')) 233 | commandLine 'cmd', '/c', file('runTestPageGen.bat').absolutePath 234 | else // assume *nix 235 | commandLine 'sh', '../runTestPageGen.sh' 236 | } 237 | tasks.register('setupPageGen', Exec) { 238 | String os = System.getProperty('os.name').toLowerCase(Locale.ROOT) 239 | if (os.contains('win')) 240 | commandLine 'cmd', '/c', 'pip install -r requirements.txt' 241 | else // assume *nix 242 | commandLine 'sh', '-c', 'pip install -r requirements.txt' 243 | } 244 | tasks.register('setupTest') { 245 | dependsOn = [deleteTestData, extractBundledFiles, generateMaven, setupPageGen, generateTestConfig, copyTestTemplates] 246 | doLast { file('./test/out').mkdirs() } 247 | } 248 | 249 | 250 | tasks.register('publishDocker', Exec) { 251 | dependsOn bundleStatic 252 | commandLine 'docker-compose', 'build' 253 | doLast { 254 | exec { commandLine 'docker', 'tag', 'pagegen:latest', 'containers.minecraftforge.net/pagegen:latest' } 255 | exec { commandLine 'docker', 'tag', 'pagegen:latest', "containers.minecraftforge.net/pagegen:$version" } 256 | exec { commandLine 'docker', 'push', 'containers.minecraftforge.net/pagegen:latest' } 257 | exec { commandLine 'docker', 'push', "containers.minecraftforge.net/pagegen:$version" } 258 | 259 | exec { commandLine 'docker', 'tag', 'pagegen-webhook:latest', 'containers.minecraftforge.net/pagegen-webhook:latest' } 260 | exec { commandLine 'docker', 'tag', 'pagegen-webhook:latest', "containers.minecraftforge.net/pagegen-webhook:$version" } 261 | exec { commandLine 'docker', 'push', 'containers.minecraftforge.net/pagegen-webhook:latest' } 262 | exec { commandLine 'docker', 'push', "containers.minecraftforge.net/pagegen-webhook:$version" } 263 | } 264 | } 265 | tasks.register('publishDockerTest', Exec) { 266 | dependsOn bundleStatic 267 | commandLine 'docker-compose', 'build' 268 | doLast { 269 | exec { commandLine 'docker', 'tag', 'pagegen:latest', 'containers.minecraftforge.net/pagegen-test:latest' } 270 | exec { commandLine 'docker', 'tag', 'pagegen:latest', "containers.minecraftforge.net/pagegen-test:$version" } 271 | exec { commandLine 'docker', 'push', 'containers.minecraftforge.net/pagegen-test:latest' } 272 | exec { commandLine 'docker', 'push', "containers.minecraftforge.net/pagegen-test:$version" } 273 | 274 | 275 | exec { commandLine 'docker', 'tag', 'pagegen-webhook:latest', 'containers.minecraftforge.net/pagegen-webhook-test:latest' } 276 | exec { commandLine 'docker', 'tag', 'pagegen-webhook:latest', "containers.minecraftforge.net/pagegen-webhook-test:$version" } 277 | exec { commandLine 'docker', 'push', 'containers.minecraftforge.net/pagegen-webhook-test:latest' } 278 | exec { commandLine 'docker', 'push', "containers.minecraftforge.net/pagegen-webhook-test:$version" } 279 | } 280 | } 281 | 282 | publishing { 283 | publications.register('forgeMaven', MavenPublication) { 284 | artifact source: bundleFiles, extension: 'zip' 285 | 286 | artifactId = 'web' 287 | 288 | pom { 289 | name = 'Forge Page Generator' 290 | description = 'Python code to generate a download frontend for maven repository' 291 | url = 'https://github.com/MinecraftForge/Web' 292 | 293 | gradleutils.pom.setGitHubDetails(pom, 'Web') 294 | 295 | license gradleutils.pom.Licenses.LGPLv2_1 296 | 297 | developers { 298 | developer gradleutils.pom.Developers.LexManos 299 | } 300 | } 301 | } 302 | 303 | repositories { 304 | maven gradleutils.publishingForgeMaven 305 | } 306 | } -------------------------------------------------------------------------------- /css/tooltipster-shadow.css: -------------------------------------------------------------------------------- 1 | .tooltipster-shadow { 2 | border-radius: 5px; 3 | background: #fff; 4 | box-shadow: 0px 0px 14px rgba(0,0,0,0.3); 5 | color: #2c2c2c; 6 | } 7 | .tooltipster-shadow .tooltipster-content { 8 | font-family: 'Arial', sans-serif; 9 | font-size: 14px; 10 | line-height: 16px; 11 | padding: 8px 10px; 12 | } -------------------------------------------------------------------------------- /css/tooltipster.css: -------------------------------------------------------------------------------- 1 | /* This is the default Tooltipster theme (feel free to modify or duplicate and create multiple themes!): */ 2 | .tooltipster-default { 3 | border-radius: 5px; 4 | border: 2px solid #000; 5 | background: #4c4c4c; 6 | color: #fff; 7 | } 8 | 9 | /* Use this next selector to style things like font-size and line-height: */ 10 | .tooltipster-default .tooltipster-content { 11 | font-family: Arial, sans-serif; 12 | font-size: 14px; 13 | line-height: 16px; 14 | padding: 8px 10px; 15 | overflow: hidden; 16 | } 17 | 18 | /* This next selector defines the color of the border on the outside of the arrow. This will automatically match the color and size of the border set on the main tooltip styles. Set display: none; if you would like a border around the tooltip but no border around the arrow */ 19 | .tooltipster-default .tooltipster-arrow .tooltipster-arrow-border { 20 | /* border-color: ... !important; */ 21 | } 22 | 23 | 24 | /* If you're using the icon option, use this next selector to style them */ 25 | .tooltipster-icon { 26 | cursor: help; 27 | margin-left: 4px; 28 | } 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | /* This is the base styling required to make all Tooltipsters work */ 38 | .tooltipster-base { 39 | padding: 0; 40 | font-size: 0; 41 | line-height: 0; 42 | position: absolute; 43 | left: 0; 44 | top: 0; 45 | z-index: 9999999; 46 | pointer-events: none; 47 | width: auto; 48 | overflow: visible; 49 | } 50 | .tooltipster-base .tooltipster-content { 51 | overflow: hidden; 52 | } 53 | 54 | 55 | /* These next classes handle the styles for the little arrow attached to the tooltip. By default, the arrow will inherit the same colors and border as what is set on the main tooltip itself. */ 56 | .tooltipster-arrow { 57 | display: block; 58 | text-align: center; 59 | width: 100%; 60 | height: 100%; 61 | position: absolute; 62 | top: 0; 63 | left: 0; 64 | z-index: -1; 65 | } 66 | .tooltipster-arrow span, .tooltipster-arrow-border { 67 | display: block; 68 | width: 0; 69 | height: 0; 70 | position: absolute; 71 | } 72 | .tooltipster-arrow-top span, .tooltipster-arrow-top-right span, .tooltipster-arrow-top-left span { 73 | border-left: 8px solid transparent !important; 74 | border-right: 8px solid transparent !important; 75 | border-top: 8px solid; 76 | bottom: -7px; 77 | } 78 | .tooltipster-arrow-top .tooltipster-arrow-border, .tooltipster-arrow-top-right .tooltipster-arrow-border, .tooltipster-arrow-top-left .tooltipster-arrow-border { 79 | border-left: 9px solid transparent !important; 80 | border-right: 9px solid transparent !important; 81 | border-top: 9px solid; 82 | bottom: -7px; 83 | } 84 | 85 | .tooltipster-arrow-bottom span, .tooltipster-arrow-bottom-right span, .tooltipster-arrow-bottom-left span { 86 | border-left: 8px solid transparent !important; 87 | border-right: 8px solid transparent !important; 88 | border-bottom: 8px solid; 89 | top: -7px; 90 | } 91 | .tooltipster-arrow-bottom .tooltipster-arrow-border, .tooltipster-arrow-bottom-right .tooltipster-arrow-border, .tooltipster-arrow-bottom-left .tooltipster-arrow-border { 92 | border-left: 9px solid transparent !important; 93 | border-right: 9px solid transparent !important; 94 | border-bottom: 9px solid; 95 | top: -7px; 96 | } 97 | .tooltipster-arrow-top span, .tooltipster-arrow-top .tooltipster-arrow-border, .tooltipster-arrow-bottom span, .tooltipster-arrow-bottom .tooltipster-arrow-border { 98 | left: 0; 99 | right: 0; 100 | margin: 0 auto; 101 | } 102 | .tooltipster-arrow-top-left span, .tooltipster-arrow-bottom-left span { 103 | left: 6px; 104 | } 105 | .tooltipster-arrow-top-left .tooltipster-arrow-border, .tooltipster-arrow-bottom-left .tooltipster-arrow-border { 106 | left: 5px; 107 | } 108 | .tooltipster-arrow-top-right span, .tooltipster-arrow-bottom-right span { 109 | right: 6px; 110 | } 111 | .tooltipster-arrow-top-right .tooltipster-arrow-border, .tooltipster-arrow-bottom-right .tooltipster-arrow-border { 112 | right: 5px; 113 | } 114 | .tooltipster-arrow-left span, .tooltipster-arrow-left .tooltipster-arrow-border { 115 | border-top: 8px solid transparent !important; 116 | border-bottom: 8px solid transparent !important; 117 | border-left: 8px solid; 118 | top: 50%; 119 | margin-top: -7px; 120 | right: -7px; 121 | } 122 | .tooltipster-arrow-left .tooltipster-arrow-border { 123 | border-top: 9px solid transparent !important; 124 | border-bottom: 9px solid transparent !important; 125 | border-left: 9px solid; 126 | margin-top: -8px; 127 | } 128 | .tooltipster-arrow-right span, .tooltipster-arrow-right .tooltipster-arrow-border { 129 | border-top: 8px solid transparent !important; 130 | border-bottom: 8px solid transparent !important; 131 | border-right: 8px solid; 132 | top: 50%; 133 | margin-top: -7px; 134 | left: -7px; 135 | } 136 | .tooltipster-arrow-right .tooltipster-arrow-border { 137 | border-top: 9px solid transparent !important; 138 | border-bottom: 9px solid transparent !important; 139 | border-right: 9px solid; 140 | margin-top: -8px; 141 | } 142 | 143 | 144 | /* Some CSS magic for the awesome animations - feel free to make your own custom animations and reference it in your Tooltipster settings! */ 145 | 146 | .tooltipster-fade { 147 | opacity: 0; 148 | -webkit-transition-property: opacity; 149 | -moz-transition-property: opacity; 150 | -o-transition-property: opacity; 151 | -ms-transition-property: opacity; 152 | transition-property: opacity; 153 | } 154 | .tooltipster-fade-show { 155 | opacity: 1; 156 | } 157 | 158 | .tooltipster-grow { 159 | -webkit-transform: scale(0,0); 160 | -moz-transform: scale(0,0); 161 | -o-transform: scale(0,0); 162 | -ms-transform: scale(0,0); 163 | transform: scale(0,0); 164 | -webkit-transition-property: -webkit-transform; 165 | -moz-transition-property: -moz-transform; 166 | -o-transition-property: -o-transform; 167 | -ms-transition-property: -ms-transform; 168 | transition-property: transform; 169 | -webkit-backface-visibility: hidden; 170 | } 171 | .tooltipster-grow-show { 172 | -webkit-transform: scale(1,1); 173 | -moz-transform: scale(1,1); 174 | -o-transform: scale(1,1); 175 | -ms-transform: scale(1,1); 176 | transform: scale(1,1); 177 | -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); 178 | -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 179 | -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 180 | -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 181 | -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 182 | transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 183 | } 184 | 185 | .tooltipster-swing { 186 | opacity: 0; 187 | -webkit-transform: rotateZ(4deg); 188 | -moz-transform: rotateZ(4deg); 189 | -o-transform: rotateZ(4deg); 190 | -ms-transform: rotateZ(4deg); 191 | transform: rotateZ(4deg); 192 | -webkit-transition-property: -webkit-transform, opacity; 193 | -moz-transition-property: -moz-transform; 194 | -o-transition-property: -o-transform; 195 | -ms-transition-property: -ms-transform; 196 | transition-property: transform; 197 | } 198 | .tooltipster-swing-show { 199 | opacity: 1; 200 | -webkit-transform: rotateZ(0deg); 201 | -moz-transform: rotateZ(0deg); 202 | -o-transform: rotateZ(0deg); 203 | -ms-transform: rotateZ(0deg); 204 | transform: rotateZ(0deg); 205 | -webkit-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 1); 206 | -webkit-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4); 207 | -moz-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4); 208 | -ms-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4); 209 | -o-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4); 210 | transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4); 211 | } 212 | 213 | .tooltipster-fall { 214 | top: 0; 215 | -webkit-transition-property: top; 216 | -moz-transition-property: top; 217 | -o-transition-property: top; 218 | -ms-transition-property: top; 219 | transition-property: top; 220 | -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); 221 | -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 222 | -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 223 | -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 224 | -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 225 | transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 226 | } 227 | .tooltipster-fall-show { 228 | } 229 | .tooltipster-fall.tooltipster-dying { 230 | -webkit-transition-property: all; 231 | -moz-transition-property: all; 232 | -o-transition-property: all; 233 | -ms-transition-property: all; 234 | transition-property: all; 235 | top: 0px !important; 236 | opacity: 0; 237 | } 238 | 239 | .tooltipster-slide { 240 | left: -40px; 241 | -webkit-transition-property: left; 242 | -moz-transition-property: left; 243 | -o-transition-property: left; 244 | -ms-transition-property: left; 245 | transition-property: left; 246 | -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1); 247 | -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 248 | -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 249 | -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 250 | -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 251 | transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15); 252 | } 253 | .tooltipster-slide.tooltipster-slide-show { 254 | } 255 | .tooltipster-slide.tooltipster-dying { 256 | -webkit-transition-property: all; 257 | -moz-transition-property: all; 258 | -o-transition-property: all; 259 | -ms-transition-property: all; 260 | transition-property: all; 261 | left: 0px !important; 262 | opacity: 0; 263 | } 264 | 265 | 266 | /* CSS transition for when contenting is changing in a tooltip that is still open. The only properties that will NOT transition are: width, height, top, and left */ 267 | .tooltipster-content-changing { 268 | opacity: 0.5; 269 | -webkit-transform: scale(1.1, 1.1); 270 | -moz-transform: scale(1.1, 1.1); 271 | -o-transform: scale(1.1, 1.1); 272 | -ms-transform: scale(1.1, 1.1); 273 | transform: scale(1.1, 1.1); 274 | } 275 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | pagegen-webhook: 4 | image: pagegen-webhook:latest 5 | build: 6 | dockerfile: Dockerfile-webhook 7 | context: . 8 | container_name: pagegen-webhook 9 | restart: 'always' 10 | environment: 11 | - TZ=America/Los_Angeles 12 | ports: 13 | - 5000:5000 14 | volumes: 15 | - ./test/maven:/in 16 | - ./test/out:/out 17 | - ./test/config:/config 18 | labels: 19 | traefik.enable: 'true' 20 | traefik.http.routers.pagegen.entrypoints: 'websecure' 21 | traefik.http.routers.pagegen.middlewares: 'pagegen-prefix' 22 | traefik.http.routers.pagegen.rule: 'Host(`webhooks.minecraftforge.net`) && PathPrefix(`/pagegen/`)' 23 | traefik.http.services.pagegen.loadbalancer.server.port: 5000 24 | traefik.http.middlewares.pagegen-prefix.stripprefix.prefixes: '/pagegen' 25 | traefik.http.middlewares.pagegen-prefix.stripprefix.forceSlash: true 26 | pagegen: 27 | image: pagegen:latest 28 | build: 29 | context: . 30 | container_name: pagegen 31 | restart: 'always' 32 | environment: 33 | - TZ=America/Los_Angeles -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /images/embed_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/embed_logo.png -------------------------------------------------------------------------------- /images/embed_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 27 | 28 | 29 | 55 | 61 | 64 | 68 | 72 | 76 | 80 | 84 | 89 | 90 | -------------------------------------------------------------------------------- /images/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /images/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /images/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #ff0000 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /images/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /images/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/favicons/favicon.ico -------------------------------------------------------------------------------- /images/favicons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Forge", 3 | "icons": [ 4 | { 5 | "src": "\/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image\/png" 8 | } 9 | ], 10 | "theme_color": "#ffffff", 11 | "display": "standalone" 12 | } 13 | -------------------------------------------------------------------------------- /images/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /images/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /images/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /images/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /images/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MinecraftForge/Web/000a9238c05e9f4bcb079260f41be59e0f8d59ae/images/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /images/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 13 | 20 | 24 | 30 | 32 | 33 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /images/logo_docs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 14 | 15 | 17 | 24 | 28 | 34 | 36 | 37 | 42 | 43 | 44 | 45 | 47 | 54 | 58 | 64 | 66 | 67 | 72 | 73 | 75 | 77 | 79 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /js/curse-ads.js: -------------------------------------------------------------------------------- 1 | // Ad blocking note 2 | 3 | var target = document.querySelector('.promo-container'); 4 | 5 | function removeNonClassAttributes(element) { 6 | var attributes = element.attributes; 7 | var i = attributes.length; 8 | while (i--) { 9 | var attr = attributes.item(i); 10 | if (attr.name !== 'class') 11 | element.removeAttributeNode(attr); 12 | } 13 | } 14 | 15 | function showBlockNote() { 16 | var blockNote = document.querySelector('.promo-container .block-note'); 17 | removeNonClassAttributes(target); 18 | removeNonClassAttributes(blockNote); 19 | blockNote.classList.add('block-note-show'); 20 | blockNote.setAttribute('style', 'display: flex !important'); 21 | target.setAttribute('style', 'display: block !important'); 22 | } 23 | 24 | function handleAttributeMutation(target, attribute) { 25 | var classes = target.classList; 26 | if (!classes.contains('promo-container') && !classes.contains('promo-content')) { 27 | return; 28 | } 29 | if (attribute === 'hidden') { 30 | showBlockNote(); 31 | } 32 | } 33 | 34 | function handleChildRemoval(children) { 35 | children.forEach( 36 | function (child) { 37 | if (child.classList.contains('promo-content')) { 38 | showBlockNote(); 39 | } 40 | } 41 | ); 42 | } 43 | 44 | // create an observer instance 45 | var observer = new MutationObserver(function (mutations) { 46 | mutations.forEach(function (mutation) { 47 | if (mutation.type === 'attributes') { 48 | handleAttributeMutation(mutation.target, mutation.attributeName); 49 | } else if (mutation.type === 'childList') { 50 | handleChildRemoval(mutation.removedNodes); 51 | } 52 | }); 53 | }); 54 | 55 | var observerConfig = {attributes: true, childList: true, characterData: true, subtree: true}; 56 | if (target) 57 | observer.observe(target, observerConfig); 58 | 59 | window.isDoneLoading = false; 60 | (window.attachEvent || window.addEventListener)('message', function (e) { 61 | document.querySelector('body').classList.add('loading-done'); 62 | window.isDoneLoading = true; 63 | }); 64 | 65 | setTimeout( 66 | function () { 67 | if (!window.isDoneLoading) { 68 | document.querySelector('body').classList.add('loading-done'); 69 | } 70 | }, 71 | 1000 72 | ); 73 | 74 | $(document).ready(function() { 75 | window.themeSwitchToggle(); 76 | }); 77 | -------------------------------------------------------------------------------- /js/docs.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | window.themeSwitchToggle(); 3 | 4 | $('pre.highlight code[class*=\'language-\']').each(function () { 5 | var className = this.className.match(/language-([A-Za-z0-9+-]+)/); 6 | if (className) { 7 | $(this).removeClass(className[0]); 8 | $(this).addClass(className[1].toLowerCase()); 9 | } 10 | }); 11 | hljs.initHighlighting(); 12 | }); 13 | -------------------------------------------------------------------------------- /js/files.js: -------------------------------------------------------------------------------- 1 | /* 2 | Implementation for various utilities on the files site. 3 | */ 4 | 5 | $(document).ready(function () { 6 | $('[data-toggle=tooltip]').tooltipster({ 7 | theme: 'tooltipster-shadow', 8 | position: 'bottom', 9 | animation: 'grow' 10 | }); 11 | $('[data-toggle=popup]').tooltipster({ 12 | theme: 'tooltipster-shadow', 13 | position: 'bottom', 14 | animation: 'grow', 15 | contentAsHTML: true, 16 | interactive: true 17 | }); 18 | $('.download-list .download-links li').each(function () { 19 | if ($(this).find('.info-tooltip').length > 0) { 20 | var info = $(this).children('.info-tooltip'); 21 | $(this).children('.info-link').tooltipster('content', info.html()); 22 | } 23 | }); 24 | $('.info-container').each(function () { 25 | if ($(this).find('.info-tooltip').length > 0) { 26 | var info = $(this).children('.info-tooltip'); 27 | var link = $(this).children('.info-link'); 28 | link.tooltipster('content', info.html()); 29 | ['delay', 'position', 'animation', 'speed'].forEach(function (key) { 30 | if (link.attr('tooltipster-' + key) !== undefined) 31 | link.tooltipster('option', key, link.attr('tooltipster-' + key)) 32 | }) 33 | } 34 | }); 35 | 36 | }); 37 | 38 | window.onload = function () { 39 | if (location.hostname != 'files.minecraftforge.net') { 40 | var find = 'files.minecraftforge.net' 41 | var replace = location.hostname 42 | 43 | if (location.protocol == 'file:') { 44 | find = /https?:\/\/files\.minecraftforge\.net/i 45 | replace = location.pathname.substring(0, location.pathname.indexOf('test/out/') + 'test/out'.length) 46 | } 47 | //console.log('Converting hostname from ' + find.toString() + ' to ' + replace) 48 | 49 | var elems = document.getElementsByTagName('a') 50 | for (var i = 0; i < elems.length; i++) 51 | elems[i]['href'] = elems[i]['href'].replace(find, replace) 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /js/google-ads.js: -------------------------------------------------------------------------------- 1 | // Deal with Google ad consent and ad blockers 2 | 3 | function giveConsent() { 4 | localStorage.consent = 'accepted'; 5 | window.themeSwitchToggle(); 6 | $('.theme-switch-wrapper').removeClass('hidden'); 7 | $('.privacy-disclaimer').remove(); 8 | (adsbygoogle = window.adsbygoogle || []).requestNonPersonalizedAds = 0; 9 | (adsbygoogle = window.adsbygoogle || []).pauseAdRequests = 0; 10 | } 11 | 12 | function revokeConsent() { 13 | localStorage.consent = 'declined'; 14 | $('.privacy-disclaimer').remove(); 15 | (adsbygoogle = window.adsbygoogle || []).requestNonPersonalizedAds = 1; 16 | (adsbygoogle = window.adsbygoogle || []).pauseAdRequests = 0; 17 | } 18 | 19 | (adsbygoogle = window.adsbygoogle || []).pauseAdRequests = 1; 20 | $('.adsbygoogle').each(function () { 21 | window.adsbygoogle.push({}); 22 | }); 23 | 24 | $(document).ready(function() { 25 | $('body').addClass('loading-done'); 26 | var consent = localStorage.consent; 27 | 28 | if (consent === 'accepted') { 29 | giveConsent(); 30 | } else if (consent === 'declined') { 31 | //$('.theme-switch-wrapper').remove(); 32 | revokeConsent(); 33 | } else if (localStorage.consent === undefined) { 34 | // Display the disclaimer by default, in case the request fails completely 35 | $('.privacy-disclaimer').css('display', 'block'); 36 | $('.theme-switch-wrapper').addClass('hidden'); 37 | $.get({url: 'https://ssl.geoplugin.net/json.gp?k=7a35ee7cc6f992e4', dataType: 'json'}) 38 | .done(function (data) { 39 | // If we're not in the EU, simply give consent, otherwise keep asking 40 | if (data.geoplugin_inEU !== 1) { 41 | giveConsent(); 42 | } 43 | }); 44 | } 45 | 46 | $('.btn-privacy-disclaimer-accept').click(function (e) { 47 | e.preventDefault(); 48 | giveConsent(); 49 | }); 50 | 51 | $('.btn-privacy-disclaimer-decline').click(function (e) { 52 | e.preventDefault(); 53 | revokeConsent(); 54 | }); 55 | 56 | function updatePrivacyStatus() { 57 | if (localStorage.consent === 'accepted') { 58 | $('.privacy-status').html('You have accepted personalized ads and usage of local storage.'); 59 | } else if (localStorage.consent === 'declined') { 60 | $('.privacy-status').html('You do not allow personalized ads or usage of local storage.'); 61 | } else { 62 | $('.privacy-status').text('You have yet to choose one of the options.') 63 | } 64 | } 65 | 66 | updatePrivacyStatus(); 67 | 68 | $('.btn-privacy-settings-accept').click(function (e) { 69 | e.preventDefault(); 70 | giveConsent(); 71 | updatePrivacyStatus(); 72 | }); 73 | 74 | $('.btn-privacy-settings-decline').click(function (e) { 75 | e.preventDefault(); 76 | revokeConsent(); 77 | updatePrivacyStatus(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /js/sidebar.js: -------------------------------------------------------------------------------- 1 | /* 2 | Implementation for sidebars used on the Forge sites. 3 | */ 4 | $(document).ready(function () { 5 | $('.scroll-pane').jScrollPane({ 6 | autoReinitialise: true, 7 | verticalGutter: 0, 8 | hideFocus: true 9 | }); 10 | $('.open-sidebar').click(function (e) { 11 | $('.sidebar-sticky').addClass('active-sidebar'); 12 | $('body').addClass('sidebar-active'); 13 | e.preventDefault(); 14 | }); 15 | $('.close-sidebar').click(function (e) { 16 | $('.sidebar-sticky').removeClass('active-sidebar'); 17 | $('body').removeClass('sidebar-active'); 18 | e.preventDefault(); 19 | }); 20 | // Collapsible elements implementation 21 | $('.collapsible,.nav-collapsible').each(function () { 22 | var item = $(this); 23 | var toggle = item.parent().find('> .toggle-collapsible'); 24 | var showText = item.data('show-text') ? item.data('show-text') : 'Show'; 25 | var hideText = item.data('hide-text') ? item.data('hide-text') : 'Hide'; 26 | var toggleStyle = item.data('toggle-style') ? item.data('toggle-style') : 'display'; 27 | var isCollapsed = function () { 28 | return toggleStyle == 'class' ? item.hasClass('collapsed') : item.css('display') == 'none'; 29 | }; 30 | var changeState = function (state) { 31 | if (toggleStyle == 'class') { 32 | if (state) { 33 | item.removeClass('collapsed'); 34 | item.addClass('expanded'); 35 | } else { 36 | item.removeClass('expanded'); 37 | item.addClass('collapsed'); 38 | } 39 | } else { 40 | item.css('display', state ? 'block' : 'none'); 41 | } 42 | }; 43 | if (!item.hasClass('nav-collapsible-open')) { 44 | changeState(false); 45 | } else { 46 | changeState(true); 47 | var icons = toggle.find('> .collapsible-icon'); 48 | var texts = toggle.find('> .collapsible-text'); 49 | icons.removeClass('fa-plus'); 50 | icons.addClass('fa-minus'); 51 | texts.html(showText); 52 | } 53 | toggle.click(function (e) { 54 | var icon = toggle.find('> .collapsible-icon'); 55 | var text = toggle.find('> .collapsible-text'); 56 | if (isCollapsed()) { 57 | changeState(true); 58 | icon.addClass('fa-minus'); 59 | icon.removeClass('fa-plus'); 60 | text.html(hideText); 61 | } else { 62 | changeState(false); 63 | icon.addClass('fa-plus'); 64 | icon.removeClass('fa-minus'); 65 | text.html(showText); 66 | } 67 | e.preventDefault(); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /js/theme-switch-toggle.js: -------------------------------------------------------------------------------- 1 | /* 2 | Enables switching themes on the fly. 3 | Initialize by calling window.themeSwitchToggle() from $(document).ready 4 | */ 5 | 6 | window.themeSwitchToggle = function () { 7 | var toggle = $('.theme-switch input'); 8 | toggle.prop('checked', localStorage.theme === window.forge.THEME_DARK); 9 | toggle.change(function () { 10 | localStorage.theme = $(this).is(':checked') ? window.forge.THEME_DARK : window.forge.THEME_LIGHT; 11 | window.forge.swapThemeCSS(localStorage.theme); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /js/theme-switch.js: -------------------------------------------------------------------------------- 1 | /* 2 | Initializes theme switching 3 | Either call before enabling toggler or in the document head 4 | */ 5 | 6 | window.forge = { 7 | THEME_LIGHT: 'light', 8 | THEME_DARK: 'dark', 9 | swapThemeCSS: function (activeTheme) { 10 | var stylesheets = document.styleSheets; 11 | var length = stylesheets.length; 12 | var i; 13 | 14 | for(i = 0; i < length; i++) { 15 | var ss = stylesheets[i]; 16 | if (!ss.ownerNode.dataset.theme) { 17 | continue; 18 | } 19 | ss.disabled = ss.ownerNode.dataset.theme !== activeTheme; 20 | } 21 | 22 | } 23 | }; 24 | 25 | if (!localStorage.theme) { 26 | localStorage.theme = window.forge.THEME_LIGHT; 27 | } 28 | 29 | window.forge.swapThemeCSS(localStorage.theme); 30 | -------------------------------------------------------------------------------- /python/generators.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import hashlib 4 | import gzip 5 | import zlib 6 | import os 7 | from abc import abstractmethod 8 | 9 | from markdown import markdown 10 | 11 | import metadata 12 | import templates 13 | from mc_version import MCVer 14 | 15 | # Writes the specified data as utf8, only if the associated md5 file is out of date 16 | # This prevents small file churn when generating indexes. Namely version specific meta.json 17 | # files will almost never change. So we don't need to rewrite them every run. 18 | # This helps disk io, as well as web IO as the metadata doesn't change and invalidate caches. 19 | def writeHashed(out, name: str, data, gzip: bool = False, print_path: bool = True): 20 | data_utf8 = data.encode('utf-8') 21 | md5_expected = hashlib.md5(data_utf8).hexdigest() 22 | file = out.joinpath(f'{name}.md5') 23 | md5_actual = None if not file.exists() else file.read_bytes().decode('utf-8') 24 | if not file.exists() or not md5_expected == md5_actual: 25 | if print_path: 26 | print(f' Writing {name} at {out}') 27 | else: 28 | print(f' Writing {name}') 29 | out.mkdir(parents=True, exist_ok=True) 30 | out.joinpath(name).write_bytes(data_utf8) 31 | file.write_text(md5_expected, 'utf-8') 32 | if gzip: 33 | write_gzip(out, name, data_utf8, print_path) 34 | 35 | 36 | def write_gzip(out, name: str, data, print_path: bool): 37 | file = out.joinpath(name) 38 | file_gz = out.joinpath(f'{name}.gz') 39 | if print_path: 40 | print(f' Gzip\'ing {name} at {out}') 41 | else: 42 | print(f' Gzip\'ing {name}') 43 | 44 | with gzip.open(file_gz, 'wb', compresslevel=9) as dst: 45 | dst.write(data) 46 | 47 | 48 | class Generator: 49 | @abstractmethod 50 | def generate(self, md: metadata.Metadata, artifact: metadata.Artifact, tpl: templates.Templates, args: argparse.Namespace): 51 | pass 52 | 53 | 54 | class MetaJsonGenerator(Generator): 55 | def generate(self, md: metadata.Metadata, artifact: metadata.Artifact, tpl: templates.Templates, args: argparse.Namespace): 56 | print(f'Writing meta.jsons') 57 | for version in artifact.all_versions: 58 | meta = {'classifiers': {cls or "null": {item.ext: item.md5} for cls, item in version.item_by_cls.items()}} 59 | out = version.path(root='output_meta') 60 | writeHashed(out, 'meta.json', json.dumps(meta, indent=2), args.gzip, True) 61 | 62 | 63 | class MavenJsonGenerator(Generator): 64 | def generate(self, md: metadata.Metadata, artifact: metadata.Artifact, tpl: templates.Templates, args: argparse.Namespace): 65 | print('Generating Maven Index') 66 | meta = {mc_vers: [value.raw_version for value in vers.values()] for mc_vers, vers in artifact.versions.items()} 67 | out = artifact.path(root='output_meta') 68 | writeHashed(out, 'maven-metadata.json', json.dumps(meta, indent=2), args.gzip) 69 | 70 | 71 | class IndexGenerator(Generator): 72 | def generate(self, md: metadata.Metadata, artifact: metadata.Artifact, tpl: templates.Templates, args: argparse.Namespace): 73 | description = markdown('\n'.join(artifact.config.get('description', []))) 74 | 75 | config_filters = artifact.config.get('filters', {}) 76 | filter_list = [{'mc': MCVer(mc_ver), 'filter': config_filters[mc_ver]} for mc_ver in sorted(config_filters, key=lambda v: MCVer(v))] 77 | out = artifact.path(root='output_web') 78 | out.mkdir(parents=True, exist_ok=True) 79 | print(f'Writing index files at {out}') 80 | render_context = {'md': md, 'artifact': artifact, 'description': description, 'filters': filter_list} 81 | template = tpl.env.get_template('base_page.html') 82 | for (file, context) in artifact.parts(render_context): 83 | writeHashed(out, file, template.render(**context), args.gzip, False) 84 | 85 | 86 | class PromoteGenerator(Generator): 87 | def generate(self, md: metadata.Metadata, artifact: metadata.Artifact, tpl: templates.Templates, args: argparse.Namespace): 88 | print(f'Promoting {artifact.name} version {args.version} to {args.type}') 89 | artifact.promote(args.version, args.type) 90 | 91 | slimpromos = { 92 | "homepage": f'{md.web_root}{artifact.mvnpath()}/', 93 | "promos": {} 94 | } 95 | for mcv, vers in artifact.promotions.items(): 96 | if mcv == 'default': 97 | for tag, v in vers.items(): 98 | slimpromos['promos'][tag] = v.lower() 99 | else: 100 | for tag, v in vers.items(): 101 | slimpromos['promos'][f'{mcv}-{tag}'] = v.lower() 102 | 103 | out = artifact.path(root='output_meta') 104 | out.mkdir(parents=True, exist_ok=True) 105 | print(f'Writing promotion files at {out}') 106 | writeHashed(out, 'promotions_slim.json', json.dumps(slimpromos, indent=2), args.gzip, False) 107 | 108 | 109 | class TrackingGenerator(Generator): 110 | def generate(self, md: metadata.Metadata, artifact: metadata.Artifact, tpl: templates.Templates, args: argparse.Namespace): 111 | out = md.path(root='output_meta') 112 | output = out.joinpath('tracked_promotions.json') 113 | print(f'Adding {artifact.name} to tracked list at {output}') 114 | tracked_promos = json.loads(output.read_text('utf-8')) if output.exists() else {} 115 | meta = { 116 | "name": artifact.fullname(), 117 | "last": { 118 | "version": artifact.all_versions[-1].version, 119 | "mc": artifact.all_versions[-1].minecraft_version, 120 | "timestamp": artifact.all_versions[-1].timestamp.timestamp() 121 | } 122 | } 123 | tracked_promos[artifact.mavenname()] = meta 124 | writeHashed(out, os.path.basename(output), json.dumps(tracked_promos, indent=2), args.gzip, False) 125 | 126 | 127 | class PromotionIndexGenerator(Generator): 128 | def generate(self, md: metadata.Metadata, artifact: metadata.Artifact, tpl: templates.Templates, args: argparse.Namespace): 129 | out = md.path(root='output_web') 130 | output = out.joinpath('project_index.html') 131 | print(f'Generating project index at {output}') 132 | promos = md.path(root='output_meta').joinpath('tracked_promotions.json') 133 | tracked_promos = json.loads(promos.read_text('utf-8')) if promos.exists() else {} 134 | template = tpl.env.get_template('project_index.html') 135 | writeHashed(out, os.path.basename(output), template.render(md=md, promos=tracked_promos), args.gzip, True) 136 | 137 | 138 | class RegenGenerator(Generator): 139 | def generate(self, md: metadata.Metadata, artifact: metadata.Artifact, tpl: templates.Templates, args: argparse.Namespace): 140 | tracked = md.path(root='output_meta').joinpath('tracked_promotions.json') 141 | print(f'Re-Generating all projects') 142 | if (not tracked.exists()): 143 | print(f'No tracked projects at {tracked}') 144 | return 145 | for key in json.loads(tracked.read_text('utf-8')): 146 | art = metadata.Artifact.load_maven_xml(md, key) 147 | for gen in Generators['gen']: 148 | gen.generate(md, art, tpl, args) 149 | for gen in Generators['index']: 150 | gen.generate(md, None, tpl, args) 151 | 152 | basegens = [MetaJsonGenerator(), MavenJsonGenerator(), IndexGenerator()] 153 | Generators: dict[str, list[Generator]] = { 154 | 'gen': basegens, 155 | 'promote': [PromoteGenerator(), TrackingGenerator(), PromotionIndexGenerator(), *basegens], 156 | 'index': [PromotionIndexGenerator()], 157 | 'regen': [RegenGenerator()] 158 | } 159 | -------------------------------------------------------------------------------- /python/mc_version.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class MCVer: 5 | type = 0 6 | week = 0 7 | year = 0 8 | pre = 0 9 | rev = None 10 | near = [] 11 | full = None 12 | special = None 13 | 14 | def __init__(self, version): 15 | self.full = version 16 | lower = version.lower().replace('-', '_') 17 | if 'default' == lower: # Not a MC Version, sort bottom 18 | pass 19 | elif '15w14a' == lower: # 2015 April Fools 20 | self.week = 14 21 | self.year = 15 22 | self.type = 3 23 | self.rev = 'a' 24 | self.near = [1, 10] 25 | self.special = Special.APRIL 26 | elif '1.rv_pre1' == lower: # 2016 April Fools 27 | self.week = 14 28 | self.year = 16 29 | self.type = 3 30 | self.rev = chr(ord('a') - 1) 31 | self.near = [1, 9, 3] 32 | self.special = Special.APRIL 33 | elif '3d shareware v1.34' == lower: # 2019 April Fools 34 | self.week = 14 35 | self.year = 19 36 | self.type = 3 37 | self.rev = chr(ord('a') - 1) 38 | self.near = [1, 14] 39 | self.special = Special.APRIL 40 | elif '20w14infinite' == lower: # 2020 April Fools 41 | self.week = 14 42 | self.year = 20 43 | self.type = 3 44 | self.rev = chr(ord('a') - 1) 45 | self.near = [1, 16] 46 | self.special = Special.APRIL 47 | elif '22w13oneblockatatime' == lower: # 2022 April Fools 48 | self.week = 13 49 | self.year = 22 50 | self.type = 3 51 | self.rev = 'b' 52 | self.near = [1, 19] 53 | self.special = Special.APRIL 54 | elif '23w13a_or_b' == lower: # 2023 April Fools 55 | self.week = 13 56 | self.year = 23 57 | self.type = 3 58 | self.rev = 'b' 59 | self.near = [1, 20] 60 | self.special = Special.APRIL 61 | elif 'inf_20100618' == lower: 62 | self.week = 25 63 | self.year = 10 64 | self.type = 1 65 | self.rev = 'a' 66 | self.near = [1, 0, 4] 67 | elif 'c0.0.13a_03' == lower: 68 | self.week = -1 69 | self.year = -1 70 | self.type = 1 71 | self.rev = chr(ord('a') - 1) 72 | self.near = [0, 0, 13] 73 | elif lower.startswith('rd_'): 74 | self.week = 20 75 | self.year = 9 76 | self.type = 1 77 | 78 | if 'rd_132211' == lower: self.rev = 'a' 79 | if 'rd_132328' == lower: self.rev = 'b' 80 | if 'rd_20090515' == lower: self.rev = 'c' 81 | if 'rd_160052' == lower: self.rev = 'd' 82 | if 'rd_161348' == lower: self.rev = 'e' 83 | 84 | self.near = [0, 0, 1] 85 | elif version[0] == 'a' or version[0] == 'b' or version[0] == 'c': 86 | clean = version[1:].replace('_', '.') 87 | self.type = 2 if version[0] == 'b' else 1 88 | if clean[-1] < '0' or clean[-1] > '9': 89 | self.rev = clean[-1] 90 | self.near = self.splitDots(clean[0:-1]) 91 | else: 92 | self.near = self.splitDots(clean) 93 | elif len(version) == 6 and version[2] == 'w': 94 | self.year = int(version[0:2]) 95 | self.week = int(version[3:5]) 96 | self.type = 3 97 | self.rev = version[5:] 98 | self.near = self.splitDots(self.fromSnapshot(self.year, self.week)) 99 | else: 100 | self.type = 4 101 | for suffix in ['_pre_release_', ' pre_release ', ' Pre-Release ', '_pre', '-pre', '-rc']: 102 | if suffix in self.full: 103 | pts = self.full.split(suffix) 104 | self.pre = int(pts[1]) 105 | if suffix == '-rc': 106 | self.pre *= -1 107 | self.near = self.splitDots(pts[0]) 108 | break 109 | 110 | if self.near == []: 111 | self.near = self.splitDots(self.full) 112 | 113 | def splitDots(self, ver): 114 | return [int(i) for i in ver.split('.')] 115 | 116 | class RangedDict(dict): 117 | def __getitem__(self, item): 118 | if not isinstance(item, range): 119 | for key in self: 120 | if item in key: 121 | return self[key] 122 | raise KeyError(item) 123 | else: 124 | return super().__getitem__(item) 125 | 126 | SNAPSHOT_RANGES = RangedDict({ 127 | range(1147, 1202): '1.1', 128 | range(1203, 1209): '1.2', 129 | range(1215, 1231): '1.3', 130 | range(1232, 1243): '1.4', 131 | range(1249, 1251): '1.4.6', 132 | range(1301, 1311): '1.5', 133 | range(1311, 1313): '1.5.1', 134 | range(1316, 1327): '1.6', 135 | range(1336, 1344): '1.7', 136 | range(1347, 1350): '1.7.4', 137 | range(1402, 1435): '1.8', 138 | range(1531, 1608): '1.9', 139 | range(1614, 1616): '1.9.3', 140 | range(1620, 1622): '1.10', 141 | range(1632, 1645): '1.11', 142 | range(1650, 1651): '1.11.1', 143 | range(1706, 1719): '1.12', 144 | range(1731, 1732): '1.12.1', 145 | range(1743, 1823): '1.13', 146 | range(1830, 1834): '1.13.1', 147 | range(1843, 1915): '1.14', 148 | range(1934, 1947): '1.15', 149 | range(2006, 2023): '1.16', 150 | range(2027, 2031): '1.16.2', 151 | range(2045, 2121): '1.17', 152 | range(2137, 2145): '1.18', 153 | range(2203, 2208): '1.18.2', 154 | range(2211, 2220): '1.19', 155 | range(2224, 2225): '1.19.1', 156 | range(2242, 2247): '1.19.3', 157 | range(2303, 2308): '1.19.4', 158 | range(2312, 9999): '1.20' 159 | }) 160 | 161 | def fromSnapshot(self, year, week): 162 | value = (year * 100) + week 163 | if ver := self.SNAPSHOT_RANGES[value]: 164 | return ver 165 | raise Exception(f'Invalid snapshot date: {value}') 166 | 167 | def compareStr(self, s1, s2): 168 | return (s1 > s2) - (s1 < s2) 169 | 170 | def compareFull(self, o): 171 | for i in range(len(self.near)): 172 | if i >= len(o.near): return 1 173 | if self.near[i] != o.near[i]: return self.near[i] - o.near[i] 174 | return 0 if len(o.near) == len(self.near) else -1 175 | 176 | def compare(self, o): 177 | if self.type != o.type: 178 | if self.type <= 2 or o.type <= 2: return self.type - o.type 179 | if self.type == 3: return -1 if self.compareFull(o) == 0 else self.compareFull(o) 180 | return 1 if self.compareFull(o) == 0 else self.compareFull(o) 181 | 182 | if self.type == 1 or self.type == 2: 183 | ret = self.compareFull(o) 184 | if ret != 0: return ret 185 | if self.rev == None and o.rev != None: return 1 186 | if self.rev != None and o.rev == None: return -1 187 | return self.compareStr(self.rev, o.rev) 188 | 189 | elif self.type == 3: 190 | if self.year != o.year: return self.year - o.year 191 | if self.week != o.week: return self.week - o.week 192 | return self.compareStr(self.rev, o.rev) 193 | 194 | ret = self.compareFull(o) 195 | if ret != 0: return ret 196 | self_pre_type = min(1, max(-1, self.pre)) 197 | o_pre_type = min(1, max(-1, o.pre)) 198 | if self_pre_type == o_pre_type: return abs(self.pre) - abs(o.pre) 199 | if self_pre_type == 0: return 1 200 | if o_pre_type == 0: return -1 201 | if self_pre_type == 1: return -1 202 | return 1 203 | 204 | def getFullRelease(self): 205 | return '.'.join(map(str, self.near)) if len(self.near) > 0 else self.full 206 | 207 | def __lt__(self, other): 208 | return self.compare(other) < 0 209 | 210 | def __gt__(self, other): 211 | return self.compare(other) > 0 212 | 213 | def __eq__(self, other): 214 | return self.compare(other) == 0 215 | 216 | def __le__(self, other): 217 | return self.compare(other) <= 0 218 | 219 | def __ge__(self, other): 220 | return self.compare(other) >= 0 221 | 222 | def __ne__(self, other): 223 | return self.compare(other) != 0 224 | 225 | def __repr__(self): 226 | return self.full 227 | 228 | class Special(Enum): 229 | APRIL = "April Fools" -------------------------------------------------------------------------------- /python/metadata.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import datetime 4 | import json 5 | import pathlib 6 | import re 7 | from collections import defaultdict 8 | from dataclasses import dataclass, field, InitVar 9 | 10 | import requests 11 | 12 | from mc_version import MCVer 13 | from pprint import pprint 14 | import xml.etree.ElementTree as elementtree 15 | 16 | MINECRAFT_FORMAT = '(?P1(?:\.\d+){1,2}?(?:[_\-](?:pre|rc)\d+)?|\d\dw\d\d\w+)' 17 | PROMOTION_REG = re.compile(r'^' + MINECRAFT_FORMAT + '-(?P[\w]+)$') 18 | VERSION_REG = re.compile(r'^(?:' + MINECRAFT_FORMAT + '-)?(?P(?:\w+(?:\.|\+))*[\d]+)-?(?P[\w\.\-]+)?$') 19 | 20 | def parse_version(version): 21 | return (versmatch.groupdict().get('mcversion') or 'default', versmatch.group('version'), versmatch.group('branch')) if (versmatch := VERSION_REG.match(version)) else ('default', version, None) 22 | 23 | 24 | def parse_artifact(artifact: str): 25 | return artifact.rsplit(":") 26 | 27 | 28 | SKIP_SUFFIXES = {'.pom', '.sha1', '.md5', '.url', '.asc', '.sha256', '.sha512', '.module'} 29 | 30 | 31 | @dataclass 32 | class Metadata: 33 | path_root: pathlib.Path 34 | output_meta: pathlib.Path 35 | output_web: pathlib.Path 36 | web_root: str 37 | dl_root: str 38 | static_root: str 39 | config_path: InitVar[pathlib.Path] = None 40 | local_data: bool = False 41 | global_config: dict = field(default_factory=dict) 42 | empty_root: pathlib.Path = pathlib.Path('/') 43 | 44 | def __post_init__(self, config_path): 45 | self.global_config = json.loads(config_path.read_bytes()) 46 | 47 | def path(self, root='path_root', *path): 48 | return getattr(self, root).joinpath(*path) 49 | 50 | 51 | @dataclass 52 | class ArtifactItem: 53 | """Artifact item""" 54 | version: ArtifactVersion 55 | filepath: pathlib.Path 56 | name: str 57 | classifier: str 58 | ext: str 59 | relative_path: str 60 | timestamp: datetime.datetime = field(init=False) 61 | sha1: str = '' 62 | md5: str = '' 63 | 64 | def __post_init__(self): 65 | self.timestamp = datetime.datetime.fromtimestamp(self.filepath.stat().st_mtime) 66 | self.sha1 = f.read_text('utf-8') if (f := self.filepath.parent.joinpath(self.name+'.sha1')).exists() else None 67 | self.md5 = f.read_text('utf-8') if (f := self.filepath.parent.joinpath(self.name+'.md5')).exists() else None 68 | 69 | @classmethod 70 | def load(cls, artifact_version, file): 71 | if file.suffix in SKIP_SUFFIXES: 72 | return None 73 | if not file.stem.startswith(artifact_version.full_name()): 74 | return None 75 | return ArtifactItem(artifact_version, file, file.name, pfx[1:] if len(pfx := file.stem.removeprefix(artifact_version.full_name())) > 0 else '', file.suffix[1:], str(file.relative_to(artifact_version.artifact.metadata.path_root))) 76 | 77 | 78 | @dataclass 79 | class ArtifactVersion: 80 | """Artifact Version""" 81 | artifact: Artifact 82 | raw_version: str 83 | version: str 84 | minecraft_version: str or None 85 | branch: str or None 86 | promotion_tags: list[str] = field(init=False, default_factory=list) 87 | items: list[ArtifactItem] = field(init=False) 88 | item_by_cls: dict[str, ArtifactItem] = field(init=False) 89 | timestamp: datetime.datetime = None 90 | 91 | @classmethod 92 | def load(cls, artifact, version): 93 | (mcvers, vers, branch) = parse_version(version) 94 | if vers.endswith('-SNAPSHOT') or branch == 'SNAPSHOT': 95 | return 96 | if not (d := artifact.path().joinpath(version)).exists(): 97 | return 98 | av = ArtifactVersion(artifact, version, vers, mcvers, branch) 99 | av.items = [ai for file in d.iterdir() if (ai := ArtifactItem.load(av, file)) is not None] 100 | if not av.items: 101 | return 102 | av.item_by_cls = {ai.classifier: ai for ai in av.items} 103 | av.timestamp = min(ai.timestamp for ai in av.items) 104 | return av 105 | 106 | def path(self, root='path_root') -> pathlib.Path: 107 | return self.artifact.path(root).joinpath(self.raw_version) 108 | 109 | def full_name(self): 110 | return f'{self.artifact.name}-{self.raw_version}' 111 | 112 | def has_promotions(self): 113 | return len(self.promotion_tags) > 0 114 | 115 | 116 | @dataclass 117 | class Artifact: 118 | """Artifact""" 119 | metadata: Metadata 120 | group: str 121 | name: str 122 | versions: dict[str, dict[str, ArtifactVersion]] = field(init=False) 123 | all_versions: list[ArtifactVersion] = field(init=False) 124 | promotions: dict = field(init=False, default_factory=lambda: defaultdict(lambda: defaultdict(str))) 125 | config: dict = field(init=False, default_factory=dict) 126 | 127 | def fullname(self): 128 | return self.config.get('name', self.name) 129 | 130 | def mavenname(self): 131 | return f'{self.group}:{self.name}' 132 | 133 | def path(self, root='path_root') -> pathlib.Path: 134 | return self.metadata.path(root, self.group.replace('.', '/'), self.name) 135 | 136 | def mvnpath(self): 137 | return self.group.replace('.', '/') + '/' + self.name 138 | 139 | def attach_promotions(self, promotions): 140 | for key, value in promotions.get('promos', {}).items(): 141 | mc_ver = 'default' 142 | tag = key.lower() 143 | 144 | if mc_match := PROMOTION_REG.match(key): 145 | mc_ver = mc_match.group('mcversion') 146 | tag = mc_match.group('tag').lower() 147 | 148 | if mc_ver in self.versions: 149 | if v := self.versions[mc_ver].get(value): 150 | if not mc_ver in self.promotions: 151 | self.promotions[mc_ver] = {} 152 | self.promotions[mc_ver][tag] = value 153 | v.promotion_tags.append(tag) 154 | 155 | def find_version(self, version: str) -> ArtifactVersion: 156 | v = next((v for v in self.all_versions if v.version == version), None) 157 | if not v: 158 | print(f'Failed to find {version} in {self.name}') 159 | print('Known Versions:') 160 | for kv in self.all_versions: 161 | print(f' {kv.version}') 162 | raise ValueError(f'Failed to find {version} in {self.name}') 163 | return v 164 | 165 | def promote(self, version, tag): 166 | (mc_vers, ver, branch) = parse_version(version) 167 | tag = tag.lower() 168 | v = self.find_version(ver) 169 | 170 | if not v.minecraft_version in self.promotions: 171 | self.promotions[v.minecraft_version] = {} 172 | 173 | if existing := self.promotions[v.minecraft_version].get(tag): 174 | self.find_version(existing).promotion_tags.remove(tag) 175 | 176 | self.promotions[v.minecraft_version][tag] = ver 177 | v.promotion_tags.append(tag) 178 | 179 | def attach_config(self, config): 180 | self.config.update(config) 181 | 182 | def parts(self, global_context: dict): 183 | if len(self.versions) > 1: 184 | sorted_mc_versions = sorted(self.versions.keys(), reverse=True, key=lambda a: MCVer(a)) 185 | release_mc_versions = dict() 186 | for mc_version in sorted_mc_versions: 187 | release_mc_version = self.get_release_milestone(MCVer(mc_version)) 188 | if release_mc_version not in release_mc_versions: 189 | release_mc_versions[release_mc_version] = [] 190 | release_mc_versions[release_mc_version].append(mc_version) 191 | first_idx = next((mc for mc in sorted_mc_versions if 'latest' in self.promotions.get(mc, [])), sorted_mc_versions[0]) 192 | yield 'index.html', global_context | {'mc_version': first_idx, 'release_mc_version': self.get_release_milestone(MCVer(first_idx)), 'mcversions': sorted_mc_versions, 'release_mcversions': release_mc_versions} 193 | for mc_version in sorted_mc_versions: 194 | yield f'index_{mc_version}.html', global_context | {'mc_version': mc_version, 'release_mc_version': self.get_release_milestone(MCVer(mc_version)), 'mcversions': sorted_mc_versions, 'release_mcversions': release_mc_versions, 'canonical_url': '' if mc_version == first_idx else f'index_{mc_version}.html'} 195 | elif len(self.versions) == 1 and not 'default' in self.versions: 196 | yield 'index.html', global_context | {'mc_version': list(self.versions.keys())[0]} 197 | else: 198 | yield 'index.html', global_context 199 | 200 | def get_release_milestone(self, release_version): 201 | if release_version.special: return release_version.special.value 202 | release_mc_version = release_version.getFullRelease() 203 | split = release_mc_version.split('.') 204 | return split[0] + '.' + split[1] if len(split) > 1 else split[0] 205 | 206 | @classmethod 207 | def load_maven_xml(cls, metadata: Metadata, artifact): 208 | (group, name) = parse_artifact(artifact) 209 | artifact = Artifact(metadata, group, name) 210 | try: 211 | # Reposilite doesn't flus the maven-metadata.xml file to disk fast enough for us to see it after a new 212 | # build is coreated. So we hackily gather the data over the web. Which is stupid. But I'll fix this 213 | # after I rebuild the reposilite stack 214 | if (metadata.local_data and (meta_file := artifact.path().joinpath('maven-metadata.xml')).exists()): 215 | versions = [v.text for v in elementtree.parse(meta_file).findall("./versioning/versions/version")] 216 | else: 217 | if (xmlresponse := requests.get(f'{metadata.dl_root}{artifact.mvnpath()}/maven-metadata.xml')).status_code == 200: 218 | versions = [v.text for v in elementtree.fromstring(xmlresponse.content).findall("./versioning/versions/version")] 219 | else: 220 | raise RuntimeError(f'Failed to read maven-metadata from {metadata.dl_root}{artifact.mvnpath()}/maven-metadata.xml') 221 | 222 | artifact.all_versions = sorted((av for v in versions if (av := ArtifactVersion.load(artifact, v)) is not None), key=lambda v: v.timestamp) 223 | mc_versions = (av.minecraft_version for av in artifact.all_versions) 224 | artifact.versions = {mc_version: {av.version: av for av in artifact.all_versions if av.minecraft_version == mc_version} for mc_version in mc_versions} 225 | promotions = {} 226 | if (promotions_file := artifact.path().joinpath('promotions_slim.json')).exists(): 227 | promotions |= json.loads(promotions_file.read_bytes()) 228 | if (promotions_file := artifact.path(root='output_meta').joinpath('promotions_slim.json')).exists(): 229 | promotions |= json.loads(promotions_file.read_bytes()) 230 | artifact.attach_promotions(promotions) 231 | # load local config 232 | if (config_file := artifact.path().joinpath('page_config.json')).exists(): 233 | artifact.attach_config(json.loads(config_file.read_bytes())) 234 | # overwrite global config values 235 | artifact.attach_config(metadata.global_config) 236 | except Exception as e: 237 | raise RuntimeError('Failed to load and parse maven-metadata.xml', e) 238 | return artifact 239 | -------------------------------------------------------------------------------- /python/page_generator.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import pathlib 3 | import sys 4 | 5 | from metadata import Metadata, Artifact 6 | from templates import Templates 7 | from generators import Generators 8 | 9 | def parse_path(f): 10 | if (p := pathlib.Path(f).absolute()).exists(): 11 | return p 12 | else: 13 | raise ValueError('The path is missing') 14 | 15 | def main(raw_args=None): 16 | parser = argparse.ArgumentParser(description='Maven based download index generator') 17 | parser.add_argument('--webout', dest='output_web', default='/out', help='Base directory to output generated index pages. Will generate in sub-directories based on the maven path', type=parse_path) 18 | parser.add_argument('--metaout', dest='output_meta', default='/out', help='Base directory to output generated metadata. Will generate in sub-directories based on the maven path', type=parse_path) 19 | parser.add_argument('--downloadroot', dest='dlroot', default='https://maven.minecraftforge.net/', help='Root URL for downloading artifacts') 20 | parser.add_argument('--webroot', dest='webroot', default='https://files.minecraftforge.net/', help='Root URL for artifact pages') 21 | parser.add_argument('--static', dest='static', default='https://files.minecraftforge.net/static/', help='Root URL for static assets used by the templates') 22 | 23 | parser.add_argument('--folder', dest='folder', default='/in/repositories/releases/', help='Root directory for the maven structure to read metadata from files', type=parse_path) 24 | parser.add_argument('--config', dest='config', default='/in/global_overrides.json', help="Location of global_overrides.json file", type=parse_path) 25 | parser.add_argument('--templates', dest='templates', default='templates', type=parse_path, help="Path to templates") 26 | parser.add_argument('--local-data', dest='localdata', default=False, action=argparse.BooleanOptionalAction) 27 | parser.add_argument('--gzip', dest='gzip', default=True, action=argparse.BooleanOptionalAction, help='Whether or not to create gzip archives of generated files') 28 | 29 | commands = parser.add_subparsers(help='Command to perform', dest='command', required=True) 30 | 31 | gen_command = commands.add_parser('gen', help='Indexes generator subcommand') 32 | gen_command.add_argument('artifact', help='Maven Artifact - net.minecraftforge:forge') 33 | 34 | index_command = commands.add_parser('index', help='Generate tracked project index') 35 | regen_command = commands.add_parser('regen', help='ReGenerate all tracked projects') 36 | 37 | promote_command = commands.add_parser('promote', help='Promote subcommand') 38 | promote_command.add_argument('artifact', help='Maven Artifact - net.minecraftforge:forge') 39 | promote_command.add_argument('version', help='Maven Version') 40 | promote_command.add_argument('type', choices=['latest', 'recommended'], help='Type of promotion') 41 | 42 | if raw_args == None: 43 | raw_args = sys.argv[1:] 44 | args = parser.parse_args(raw_args) 45 | 46 | if (args.webroot[-1] != '/'): args.webroot += '/' 47 | if (args.dlroot[-1] != '/'): args.dlroot += '/' 48 | if (args.static[-1] != '/'): args.static += '/' 49 | 50 | print('Page Generator:') 51 | print(f'PyVer: {sys.version}') 52 | print(f'Folder: {args.folder}') 53 | print(f'Config: {args.config}') 54 | print(f'Web Out: {args.output_web}') 55 | print(f'Meta Out: {args.output_meta}') 56 | print(f'WebRoot: {args.webroot}') 57 | print(f'LocalData:{args.localdata}') 58 | print(f'Gzip: {args.gzip}') 59 | print(f'DLRoot: {args.dlroot}') 60 | print(f'Static: {args.static}') 61 | print(f'Templates:{args.templates}') 62 | print(f'Command: {args.command}') 63 | print(f'Artifact: {args.artifact if "artifact" in args else None}') 64 | print(f'Version: {args.version if "version" in args else None}') 65 | print(f'Type: {args.type if "type" in args else None}') 66 | 67 | metadata = Metadata(args.folder, args.output_meta, args.output_web, args.webroot, args.dlroot, args.static, args.config, args.localdata) 68 | artifact = Artifact.load_maven_xml(metadata, args.artifact) if 'artifact' in args else None 69 | templates = Templates(args.templates, args.static, args.webroot, args.dlroot) 70 | 71 | for gen in Generators[args.command]: 72 | gen.generate(metadata, artifact, templates, args) 73 | 74 | print('Finished') 75 | 76 | if __name__ == '__main__': 77 | main() 78 | -------------------------------------------------------------------------------- /python/page_generator_service.py: -------------------------------------------------------------------------------- 1 | import waitress 2 | import logging 3 | import os 4 | import time 5 | import json 6 | import base64 7 | from threading import Thread 8 | from enum import Enum 9 | from flask import Flask, request 10 | from queue import PriorityQueue 11 | from pprint import pprint 12 | from paste.translogger import TransLogger 13 | 14 | import page_generator 15 | 16 | logging.basicConfig(level = logging.NOTSET) 17 | logging.getLogger('waitress').setLevel(logging.INFO) 18 | 19 | logger = logging.getLogger('pagegen') 20 | logger.setLevel(logging.DEBUG) 21 | 22 | app = Flask(__name__) 23 | app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 # Posts should never need more then 1KB 24 | 25 | tasks = PriorityQueue() 26 | users = {} 27 | config = {} 28 | 29 | TaskTypes = Enum('TaskTypes', ['PROMOTE', 'REGEN_INDEX', 'GEN', 'REGEN']) 30 | 31 | class Task: 32 | def __init__(self, type, data): 33 | self.type = type 34 | self.data = data 35 | 36 | def __repr__(self): 37 | return "Task(type={}, data={})".format(self.type, self.data) 38 | 39 | def __gt__(self, other): 40 | return self.type.value > other.type.value 41 | def __lt__(self, other): 42 | return self.type.value < other.type.value 43 | def __eq__(self, other): 44 | return self.type.value == other.type.value 45 | 46 | @app.route('/promote', methods = ['POST']) 47 | @app.route('/promote////', methods = ['GET']) 48 | def promote(type=None, group=None, artifact=None, version=None): 49 | data = {} 50 | if request.method == 'GET': 51 | data = { 52 | 'type': type, 53 | 'group': group.replace('/', '.'), 54 | 'artifact': artifact, 55 | 'version': version 56 | } 57 | elif request.method == 'POST': 58 | data = request.get_json() 59 | else: 60 | return 'Unsupported Request', 404 61 | 62 | if data.get('type') == None or data.get('group') == None or data.get('artifact') == None or data.get('version') == None: 63 | return "Missing promotion target: {}".format(data), 400 64 | 65 | if not data['type'] in ['latest', 'recommended']: 66 | return "Invalid promotion type {} only latest or recommended supported".format(data["type"]), 400 67 | 68 | loginResponse = checkAccess(data) 69 | if loginResponse: 70 | return loginResponse 71 | 72 | logger.info("Queueing Promotion: {}:{}:{} {}".format(data['group'], data['artifact'], data['version'], data['type'])) 73 | tasks.put(Task(TaskTypes.PROMOTE, data)) 74 | 75 | return 'Promotion Queued' 76 | 77 | @app.route('/gen', methods = ['POST']) 78 | @app.route('/gen//', methods = ['GET']) 79 | def gen(group=None, artifact=None): 80 | data = {} 81 | if request.method == 'GET': 82 | data = { 83 | 'group': group.replace('/', '.'), 84 | 'artifact': artifact 85 | } 86 | elif request.method == 'POST': 87 | data = request.get_json() 88 | else: 89 | return 'Unsupported Request', 404 90 | 91 | if data.get('group') == None or data.get('artifact') == None: 92 | return "Missing generator target: {}".format(data), 400 93 | 94 | loginResponse = checkAccess(data) 95 | if loginResponse: 96 | return loginResponse 97 | 98 | logger.info("Queueing Gen: {}:{}".format(data['group'], data['artifact'])) 99 | tasks.put(Task(TaskTypes.GEN, data)) 100 | 101 | return 'Generation Queued' 102 | 103 | @app.route('/regen', methods = ['GET']) 104 | def regen(): 105 | loginResponse = checkAccess({'group': '', 'artifact': ''}) 106 | if loginResponse: 107 | return loginResponse 108 | 109 | logger.info("Queueing Complete Regen") 110 | tasks.put(Task(TaskTypes.REGEN, {})) 111 | 112 | return 'Complete Generation Queued' 113 | 114 | @app.route('/regen-index', methods = ['GET']) 115 | def regen_index(): 116 | loginResponse = checkAccess({'group': '', 'artifact': ''}) 117 | if loginResponse: 118 | return loginResponse 119 | 120 | logger.info("Queueing Index Regen") 121 | tasks.put(Task(TaskTypes.REGEN_INDEX, {})) 122 | 123 | return 'Index Generation Queued' 124 | 125 | def process_queue(): 126 | while tasks: 127 | task = tasks.get() 128 | logger.info(task) 129 | data = task.data 130 | key = task.type.name.lower() 131 | args = [] 132 | if key in config and 'args' in config[key]: 133 | args += config[key]['args'] 134 | 135 | if task.type == TaskTypes.PROMOTE: 136 | logger.info('Promoting %s:%s', data['group'], data['artifact']) 137 | args += ['promote', data['group'] + ':' + data['artifact'], data['version'], data['type']] 138 | elif task.type == TaskTypes.REGEN: 139 | logger.info('Regnerating Everything') 140 | args += ['regen'] 141 | elif task.type == TaskTypes.REGEN_INDEX: 142 | logger.info('Regnerating Tracked Index') 143 | args += ['index'] 144 | elif task.type == TaskTypes.GEN: 145 | logger.info('Generating %s:%s', data['group'], data['artifact']) 146 | args += ['gen', data['group'] + ':' + data['artifact']] 147 | 148 | try: 149 | page_generator.main(args) 150 | except Exception as e: 151 | logger.error('Failed to run generator: %s', e) 152 | 153 | logger.error("Task queue ended") 154 | 155 | def load_users(): 156 | global users 157 | logger.info('Loading users...') 158 | if not os.path.isfile('/config/users.json'): 159 | raise 'Missing /config/users.json, No authentication will work' 160 | 161 | with open('/config/users.json', 'r') as f: 162 | users = json.load(f) 163 | 164 | logger.info("Loaded %d users", len(users)) 165 | 166 | def load_config(): 167 | global config 168 | logger.info('Loading config...') 169 | if not os.path.isfile('/config/config.json'): 170 | logger.warning('Missing /config/config.json, Its non critical for now') 171 | return 172 | 173 | with open('/config/config.json', 'r') as f: 174 | config = json.load(f) 175 | 176 | logger.info('Loaded config file') 177 | 178 | def checkAccess(data): 179 | global users 180 | 181 | if not 'Authorization' in request.headers: 182 | logger.info('Ignoring anonymous user') 183 | return '', 401, {'WWW-Authenticate': 'BASIC realm="Forge Maven"'} 184 | 185 | auth = request.headers.get('Authorization') 186 | 187 | if (auth.startswith('Basic ')): 188 | try: 189 | auth = base64.b64decode(auth[6:]).decode('utf-8') 190 | except TypeError: 191 | return 'Failed to decode base auth', 401, {'WWW-Authenticate': 'BASIC realm="Forge Maven"'} 192 | 193 | user,password = auth.split(':', 1) 194 | if not user in users: 195 | logger.info("Unknown user: %s", user) 196 | return 'Unauthorized', 401, {'WWW-Authenticate': 'BASIC realm="Forge Maven"'} 197 | 198 | if not users[user]['password'] == password: 199 | logger.info('User %s failed to login, invalid password', user) 200 | return 'Unauthorized', 401, {'WWW-Authenticate': 'BASIC realm="Forge Maven"'} 201 | 202 | success = False 203 | for access in users[user]['access']: 204 | if access['group'] == data['group'] or access['group'] == '': 205 | if access['artifact'] == data['artifact'] or access['artifact'] == '': 206 | success = True 207 | break 208 | 209 | if not success: 210 | logger.info('User %s does not have access to %s:%s', user, data['group'], data['artifact']) 211 | return 'Unauthorized', 401, {'WWW-Authenticate': 'BASIC realm="Forge Maven"'} 212 | 213 | logger.info('User %s permitted to access %s:%s', user, data['group'], data['artifact']) 214 | return None 215 | 216 | if __name__ == "__main__": 217 | load_config() 218 | load_users() 219 | 220 | processor = Thread(target=process_queue) 221 | processor.start() 222 | #app = TransLogger(app, setup_console_handler=False) 223 | waitress.serve(app, host='0.0.0.0', port=5000) -------------------------------------------------------------------------------- /python/templates.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import itertools 3 | import pathlib 4 | import re 5 | import requests 6 | import zlib 7 | 8 | import jinja2 9 | import markupsafe 10 | 11 | from mc_version import MCVer 12 | from metadata import Artifact 13 | 14 | @jinja2.pass_context 15 | def show_classifier(context, mc_version, classifier): 16 | filters = context.parent['filters'] 17 | mc_vers = MCVer(mc_version) 18 | filter = next(reversed([f['filter'] for f in filters if mc_vers >= f['mc']]), []) 19 | return classifier not in filter 20 | 21 | 22 | def humanformatdate(dt: datetime.datetime): 23 | return markupsafe.Markup(f'') 24 | 25 | 26 | def get_artifact_description(artifact: Artifact, mc_version): 27 | if mc_version: 28 | hdr = (f'Downloads for {artifact.fullname()} for Minecraft {mc_version}',) 29 | lines = (f'{t.capitalize()}: {v}' for t, v in artifact.promotions[mc_version].items()) 30 | else: 31 | hdr = (f'Downloads for {artifact.fullname()}',) 32 | lines = (f'Latest: {artifact.all_versions[-1].version}',) 33 | 34 | return '\n'.join(itertools.chain(hdr, lines)) 35 | 36 | def crc32(file_path: str, chunk_size: int = 4096) -> str: 37 | """Computes the CRC32 checksum of the contents of the file at the given file_path""" 38 | checksum = 0 39 | with open('./static/' + file_path, 'rb') as f: 40 | while (chunk := f.read(chunk_size)): 41 | checksum = zlib.crc32(chunk, checksum) 42 | 43 | return "%08X" % (checksum & 0xFFFFFFFF) 44 | 45 | class Templates: 46 | def __init__(self, template_path: pathlib.Path, static_base, web_base, repository_base): 47 | self.env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_path)) 48 | self.env.globals['static_root'] = static_base 49 | self.env.globals['web_base'] = web_base 50 | self.env.globals['repository_base'] = repository_base 51 | now = datetime.datetime.utcnow() 52 | self.env.globals['now'] = now 53 | self.env.globals['crc32'] = crc32 54 | self.env.globals['show_classifier'] = show_classifier 55 | self.env.globals['get_artifact_description'] = get_artifact_description 56 | 57 | self.env.filters['humanformatdate'] = humanformatdate 58 | self.env.filters['formatdate'] = lambda dt: f'{dt:%Y-%m-%d %H:%M:%S}' 59 | self.env.filters['formatdatesimple'] = lambda dt: f'{dt:%Y-%m-%d}' 60 | self.env.filters['todatetime'] = lambda f: datetime.datetime.fromtimestamp(f) 61 | self.env.filters['maventopath'] = lambda p: '/'.join(re.split(r"[:.]", p)) 62 | 63 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2~=3.1.2 2 | Markdown~=3.5 3 | MarkupSafe~=2.0 4 | requests~=2.28.1 5 | flask~=3.0.0 6 | waitress~=2.1.2 7 | paste~=3.6.0 -------------------------------------------------------------------------------- /runTestPageGen.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set WEB_ROOT=https://files.minecraftforge.net 3 | set DOWNLOAD_ROOT=https://maven.minecraftforge.net 4 | set STATIC_DIR=file://%cd:\=/%/static/ 5 | set ARGS=./python/page_generator.py --webout "./out/" --metaout "./out" --config "./config/global_overrides.json" --webroot "%WEB_ROOT%" --downloadroot "%DOWNLOAD_ROOT%" --static "%STATIC_DIR%" --folder "./maven" --templates "./templates/" --local-data 6 | python %ARGS% promote net.minecraftforge:forge 1.16.5-36.1.0 recommended 7 | python %ARGS% promote net.minecraftforge:forge 1.16.5-36.1.2 latest 8 | python %ARGS% promote net.minecraftforge:forge 1.18.1-39.1.0 recommended 9 | python %ARGS% promote net.minecraftforge:forge 1.18.1-39.1.2 latest 10 | python %ARGS% promote net.minecraftforge:forge 1.18.2-40.0.8 latest 11 | python %ARGS% promote net.minecraftforge:froge 23w13a_or_b-april.2023.13.b.8 latest 12 | python %ARGS% promote net.minecraftforge:froge 23w17a-2023.17.a.8 latest 13 | python %ARGS% promote net.minecraftforge:froge 1.20.1-pre1-46.0.8 latest 14 | python %ARGS% promote net.minecraftforge:froge 1.19.4-45.0.8 latest 15 | python %ARGS% promote net.minecraftforge:froge 1.20-rc1-46.1.8 latest -------------------------------------------------------------------------------- /runTestPageGen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ARGS="./python/page_generator.py --webout ./out/ --metaout ./out --config ./config/global_overrides.json --webroot https://files.minecraftforge.net --downloadroot https://maven.minecraftforge.net --static file://$(pwd)/static/ --folder ./maven --templates ./templates/ --local-data" 3 | python3 $ARGS promote net.minecraftforge:forge 1.16.5-36.1.0 recommended 4 | python3 $ARGS promote net.minecraftforge:forge 1.16.5-36.1.2 latest 5 | python3 $ARGS promote net.minecraftforge:forge 1.18.1-39.1.0 recommended 6 | python3 $ARGS promote net.minecraftforge:forge 1.18.1-39.1.2 latest 7 | python3 $ARGS promote net.minecraftforge:forge 1.18.2-40.0.8 latest 8 | python3 $ARGS promote net.minecraftforge:froge 23w13a_or_b-april.2023.13.b.8 latest 9 | python3 $ARGS promote net.minecraftforge:froge 23w17a-2023.17.a.8 latest 10 | python3 $ARGS promote net.minecraftforge:froge 1.20.1-pre1-46.0.8 latest 11 | python3 $ARGS promote net.minecraftforge:froge 1.19.4-45.0.0 latest -------------------------------------------------------------------------------- /sass/_ads.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Advertisement stylesheets applicable everywhere. 3 | They respond to the display size and hence support responsive ads from Google. 4 | */ 5 | .promo, .ad { 6 | position: relative; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | background: #fafafa; 11 | overflow: hidden; 12 | } 13 | 14 | .promo-container, .ad-container { 15 | &-framed { 16 | padding: 1rem; 17 | box-sizing: content-box; 18 | background: $section-bg; 19 | border-radius: 3px; 20 | border: 1px solid $section-highlight; 21 | 22 | &:before { 23 | content: 'ADVERTISEMENT'; 24 | padding-bottom: 0.25rem; 25 | position: relative; 26 | font-weight: bold; 27 | font-size: 1.1rem; 28 | display: block; 29 | text-align: left; 30 | } 31 | 32 | .promo { 33 | margin-bottom: 0 !important; 34 | } 35 | } 36 | } 37 | 38 | .promo, .ad { 39 | &-desktop-300x600 { 40 | min-width: 300px; 41 | width: 300px; 42 | max-width: 300px; 43 | height: 600px; 44 | } 45 | 46 | &-desktop-160x600 { 47 | min-width: 160px; 48 | width: 160px; 49 | max-width: 160px; 50 | height: 600px; 51 | } 52 | 53 | &-desktop-728x90 { 54 | min-width: 728px; 55 | width: 728px; 56 | max-width: 728px; 57 | height: 90px; 58 | margin-left: auto; 59 | margin-right: auto; 60 | } 61 | 62 | &-dummy { 63 | &:before { 64 | content: 'AD'; 65 | position: absolute; 66 | top: 0; 67 | left: 0; 68 | right: 0; 69 | bottom: 0; 70 | font-size: 1.2rem; 71 | line-height: 1.2rem; 72 | padding: 0.4rem; 73 | } 74 | } 75 | } 76 | 77 | // Warning message for enabled ad blockers 78 | .promo-container .block-note, .ad-container .block-note { 79 | font-size: 1.4rem; 80 | display: none; 81 | padding: 0.5rem; 82 | align-items: center; 83 | 84 | .block-note-icon { 85 | font-size: 4rem; 86 | margin-right: 1rem; 87 | color: #f64627; 88 | } 89 | 90 | &.block-note-show { 91 | display: flex !important; 92 | } 93 | } 94 | 95 | /* 96 | // Ad block detection 97 | .loading-done .promo-container { 98 | // This generally detects various methods of ad blocking, such as simply hiding an element, blocking loading and removing it 99 | .block-note:only-child, 100 | .promo-content:not([data-google-query-id]) + .block-note, 101 | .promo-content[hidden] + .block-note, 102 | *:not(.promo-content) + .block-note { 103 | display: flex !important; 104 | } 105 | 106 | .promo-content:not([data-google-query-id]) { 107 | display: none !important; 108 | } 109 | } 110 | 111 | .loading-done .ad-container { 112 | // This generally detects various methods of ad blocking, such as simply hiding an element, blocking loading and removing it 113 | .block-note:only-child, 114 | .adsbygoogle:not([data-adsbygoogle-status]) + .block-note, 115 | .adsbygoogle[hidden] + .block-note, 116 | *:not(.adsbygoogle) + .block-note { 117 | display: flex !important; 118 | } 119 | 120 | .adsbygoogle:not([data-adsbygoogle-status]) { 121 | display: none !important; 122 | } 123 | } 124 | */ 125 | 126 | @media screen and (max-width: $mobile-break) { 127 | .promo, .ad { 128 | &-desktop-300x600, &-desktop-160x600 { 129 | min-width: 300px; 130 | width: 300px; 131 | max-width: 300px; 132 | min-height: 250px; 133 | height: 250px; 134 | max-height: 250px; 135 | margin-left: auto; 136 | margin-right: auto; 137 | } 138 | } 139 | 140 | .promo-container .block-note, .ad-container .block-note { 141 | flex-direction: column; 142 | 143 | .block-note-icon { 144 | margin-right: 0; 145 | margin-bottom: 1rem; 146 | } 147 | } 148 | } 149 | 150 | @media screen and (max-width: 767px) { 151 | .promo, .ad { 152 | &-desktop-300x600, &-desktop-160x600 { 153 | min-width: 300px; 154 | width: 300px; 155 | max-width: 300px; 156 | min-height: 250px; 157 | height: 250px; 158 | max-height: 250px; 159 | margin-left: auto; 160 | margin-right: auto; 161 | } 162 | 163 | &-desktop-728x90 { 164 | min-width: 320px; 165 | width: 320px; 166 | max-width: 320px; 167 | min-height: 100px; 168 | height: 100px; 169 | max-height: 100px; 170 | margin-left: auto; 171 | margin-right: auto; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /sass/_base.scss: -------------------------------------------------------------------------------- 1 | $font-family-base: 'Source Sans Pro', sans-serif !default; 2 | $font-size-root: 14px !default; 3 | $font-size-base: 1rem !default; 4 | $line-height-base: 1.4 !default; 5 | $font-size-code: 0.8rem !default; 6 | 7 | $table-bg: transparent !default; 8 | $table-cell-padding: .75rem !default; 9 | 10 | $abbr-border-color: #818a91 !default; 11 | $dt-font-weight: bold !default; 12 | $cursor-disabled: not-allowed !default; 13 | 14 | $mobile-break: 1020px; -------------------------------------------------------------------------------- /sass/_docs.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Roboto+Slab:300,400,700"); 2 | 3 | .docs-entry { 4 | background: $section-bg; 5 | box-shadow: 0 1px 2px $section-highlight; 6 | border-radius: 3px; 7 | padding: 1rem; 8 | overflow-x: auto; 9 | width: 100%; 10 | max-width: 100%; 11 | 12 | h1, h2, h3, h4, h5, h6 { 13 | margin: 0 0 0.5rem; 14 | font-family: "Roboto Slab", serif; 15 | 16 | code { 17 | background: none; 18 | border: none; 19 | padding: 0 0.25rem; 20 | font-size: inherit; 21 | font-weight: normal; 22 | color: inherit; 23 | } 24 | } 25 | 26 | h1, h2 { 27 | border-bottom: 1px solid $section-highlight; 28 | padding-bottom: 0.625rem; 29 | } 30 | 31 | h1 { 32 | font-size: 1.6rem; 33 | } 34 | 35 | h2 { 36 | font-size: 1.4rem; 37 | margin: 1rem 0 0.5rem; 38 | } 39 | 40 | h3 { 41 | color: $accent2; 42 | font-weight: normal; 43 | } 44 | 45 | ul, ol, dl { 46 | padding-left: 1.4rem; 47 | } 48 | 49 | p, pre, table { 50 | margin-bottom: 0.625rem; 51 | } 52 | 53 | p, ul, ol, dl { 54 | text-align: justify; 55 | 56 | code { 57 | white-space: pre; 58 | word-wrap: normal; 59 | } 60 | } 61 | } 62 | 63 | .docs-entry-footer-buttons { 64 | margin-top: 1rem; 65 | } 66 | 67 | section.sidebar-nav > ul { 68 | .elem-toc { 69 | background: rgba(0, 0, 0, 0.05); 70 | padding: 0 !important; 71 | 72 | & > .elem-text { 73 | padding: 0.2rem 0.5rem; 74 | } 75 | } 76 | 77 | .table-of-contents { 78 | background: none !important; 79 | font-size: 0.9rem !important; 80 | 81 | .toc-item, .toc-subitem { 82 | padding-left: 1rem; 83 | } 84 | } 85 | } 86 | 87 | .float-right { 88 | float: right; 89 | } 90 | 91 | .float-left { 92 | float: left; 93 | } 94 | 95 | .admonition { 96 | border-radius: 3px; 97 | box-shadow: 0 1px 2px $section-highlight; 98 | margin-bottom: 0.625rem; 99 | overflow-y: auto; // Allow child margin to affect admonition height 100 | 101 | p { 102 | padding: 0 1rem; 103 | } 104 | 105 | pre { 106 | margin-left: 1rem; 107 | margin-right: 1rem; 108 | } 109 | 110 | .admonition-title { 111 | font-weight: bold; 112 | padding: 0.2rem 1rem; 113 | font-size: 1.2rem; 114 | color: $pagination-color; 115 | margin: 0; 116 | border-radius: 3px 3px 0 0; 117 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 118 | } 119 | 120 | & > *:nth-child(2) { 121 | margin-top: 0.5rem; 122 | } 123 | 124 | // Admonition colors are constructed using the base theme colors 125 | @function admonition-bg($hue) { 126 | @return hsl($hue, saturation($accent5), lightness($scrollbar-track-bg)); 127 | } 128 | 129 | @function admonition-title-bg($hue) { 130 | $base: admonition-bg($hue); 131 | @return saturate(hsl(hue($base), saturation($base), lightness($scrollbar-drag-bg)), 5%); 132 | } 133 | 134 | @mixin admonition($hue) { 135 | background: admonition-bg($hue); 136 | 137 | .admonition-title { 138 | background: admonition-title-bg($hue); 139 | } 140 | } 141 | 142 | &, &.note { 143 | @include admonition(206); 144 | } 145 | 146 | &.important { 147 | @include admonition(168); 148 | } 149 | 150 | &.warning, &.attention { 151 | @include admonition(30); 152 | } 153 | } 154 | 155 | .injected { 156 | display: none; 157 | } 158 | 159 | // Highlight JS 160 | .hljs { 161 | display: block; 162 | overflow-x: auto; 163 | -webkit-text-size-adjust: none 164 | } 165 | 166 | .headerlink { 167 | display: inline-block; 168 | margin-left: 0.5rem; 169 | font: normal normal normal 14px/1 FontAwesome; 170 | font-size: inherit; 171 | text-rendering: auto; 172 | -webkit-font-smoothing: antialiased; 173 | -moz-osx-font-smoothing: grayscale; 174 | 175 | &:before { 176 | content: "\f0c1"; 177 | } 178 | } 179 | 180 | .sidebar-heading { 181 | display: flex; 182 | flex-direction: row; 183 | align-items: center; 184 | } 185 | 186 | .version-selection { 187 | display: flex; 188 | flex-direction: row; 189 | margin-left: auto; 190 | 191 | label { 192 | margin-bottom: 0; 193 | } 194 | 195 | select.version-selection-dropdown { 196 | color: $pagination-color; 197 | background: $accent1; 198 | border: none; 199 | border-bottom: 2px solid $pagination-color; 200 | 201 | &:active, &:focus { 202 | border-bottom-color: $accent7; 203 | outline: none; 204 | } 205 | 206 | option { 207 | background: $accent1; 208 | } 209 | } 210 | } 211 | 212 | @media screen and (max-width: $mobile-break) { 213 | table { 214 | display: block; 215 | overflow-x: auto; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /sass/_downloads.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Styles specific to the downloads page 3 | */ 4 | .promos-wrapper { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | 9 | .promos-content { 10 | align-self: stretch; 11 | } 12 | } 13 | 14 | .downloads { 15 | position: relative; 16 | display: flex; 17 | justify-content: center; 18 | padding: 0; 19 | align-items: flex-start; 20 | flex-wrap: wrap; 21 | } 22 | 23 | .downloads div.download { 24 | min-width: 350px; 25 | max-width: 100%; 26 | color: #eeeeee; 27 | padding: 0; 28 | position: relative; 29 | text-decoration: none; 30 | transition: background 0.2s ease-in-out; 31 | text-align: center; 32 | font-size: 1.5rem; 33 | box-shadow: 0 1px 2px $section-highlight; 34 | border-radius: 3px; 35 | margin: 0 1rem 1rem; 36 | 37 | small { 38 | font-size: 1rem; 39 | } 40 | 41 | i { 42 | position: relative; 43 | margin-right: 5px; 44 | top: 2px; 45 | } 46 | 47 | .title { 48 | position: relative; 49 | background-color: $accent1; 50 | border-radius: 3px 3px 0 0; 51 | padding: 10px; 52 | line-height: 1.2rem; 53 | } 54 | 55 | .links { 56 | position: relative; 57 | background: $section-highlight; 58 | border-radius: 0 0 3px 3px; 59 | border-top: 1px solid $section-highlight; 60 | margin: 0; 61 | display: grid; 62 | grid-gap: 1px; 63 | grid-auto-flow: column; 64 | grid-template-rows: 1fr 1fr; 65 | grid-auto-columns: 1fr; 66 | 67 | .link { 68 | background: $section-bg; 69 | } 70 | 71 | .link-boosted { 72 | grid-column-end: span 2; 73 | grid-row-end: span 2; 74 | 75 | a { 76 | font-size: 1.5rem; 77 | } 78 | } 79 | 80 | a { 81 | position: relative; 82 | box-sizing: border-box; 83 | font-size: 1rem; 84 | width: 100%; 85 | height: 100%; 86 | text-decoration: none; 87 | display: flex; 88 | flex-direction: column; 89 | align-items: center; 90 | justify-content: center; 91 | padding: 0.75rem; 92 | color: $body-color; 93 | transition: background 0.2s ease-in-out; 94 | 95 | &:hover { 96 | background-color: rgba(0, 0, 0, 0.1); 97 | } 98 | 99 | &:active { 100 | background-color: rgba(0, 0, 0, 0.1); 101 | box-shadow: inset 0 2px 3px rgba(0, 0, 0, 0.2); 102 | } 103 | 104 | i { 105 | width: 28px; 106 | margin: 0; 107 | font-size: 1.75em; 108 | } 109 | 110 | .promo-label { 111 | display: block; 112 | margin-top: 5px; 113 | font-size: 0.8em; 114 | } 115 | } 116 | 117 | &-split { 118 | background: $section-bg; 119 | display: flex; 120 | align-items: stretch; 121 | 122 | .link { 123 | flex-grow: 1; 124 | } 125 | 126 | a { 127 | border-right: 1px solid $section-highlight; 128 | } 129 | 130 | .link:first-child a { 131 | border-bottom-left-radius: 3px; 132 | } 133 | 134 | .link:last-child a { 135 | border-bottom-right-radius: 3px; 136 | border-right: 0; 137 | } 138 | 139 | .link-left, .links-right { 140 | flex-grow: 1; 141 | flex-shrink: 0; 142 | flex-basis: 0; 143 | } 144 | 145 | .link-left { 146 | position: relative; 147 | 148 | a { 149 | border-bottom-left-radius: 3px; 150 | font-size: 1.5rem; 151 | position: absolute; 152 | top: 0; 153 | left: 0; 154 | bottom: 0; 155 | right: 0; 156 | } 157 | } 158 | 159 | .links-right { 160 | display: flex; 161 | flex-direction: row; 162 | flex-wrap: wrap; 163 | 164 | .link { 165 | flex-shrink: 0; 166 | flex-basis: 60px; 167 | 168 | a { 169 | padding: 0.7rem; 170 | } 171 | } 172 | 173 | .link:nth-child(2n) a { 174 | border-right: none; 175 | } 176 | 177 | .link:nth-child(n + 3) a { 178 | border-top: 1px solid $section-highlight; 179 | } 180 | 181 | .link:last-child a { 182 | border-bottom-right-radius: 3px; 183 | } 184 | } 185 | } 186 | } 187 | } 188 | 189 | .download-list { 190 | i { 191 | font-size: 0.875rem; 192 | } 193 | 194 | td:first-child { 195 | border-right: 1px solid $section-highlight; 196 | } 197 | 198 | .download-version { 199 | font-weight: bold; 200 | color: $accent2; 201 | } 202 | 203 | .download-links { 204 | margin: 0; 205 | padding: 0; 206 | display: flex; 207 | list-style-type: none; 208 | 209 | li { 210 | padding: 0 0.5rem; 211 | border-right: 1px solid $section-highlight; 212 | 213 | .download-classifier { 214 | margin-right: 0.25rem; 215 | } 216 | 217 | &:first-child { 218 | padding-left: 0; 219 | } 220 | 221 | &:last-child { 222 | padding-right: 0; 223 | border-right: none; 224 | } 225 | } 226 | 227 | a { 228 | color: $body-color; 229 | text-decoration: underline; 230 | 231 | &:hover { 232 | color: lighten($body-color, 5%); 233 | text-decoration: none; 234 | } 235 | } 236 | } 237 | } 238 | 239 | .download-container { 240 | display: flex; 241 | flex-direction: column; 242 | align-items: center; 243 | 244 | .btn { 245 | margin-bottom: 1rem; 246 | } 247 | 248 | .download-list-wrapper { 249 | width: 100%; 250 | } 251 | } 252 | 253 | .download-disclaimer { 254 | width: 100%; 255 | margin-bottom: 0.5rem; 256 | 257 | h2 { 258 | margin: 0; 259 | } 260 | } 261 | 262 | .fa[class*="classifier-"]:before { 263 | content: "\f1c6"; 264 | } 265 | 266 | .fa.classifier-installer:before { 267 | content: "\f187"; 268 | } 269 | 270 | .fa.classifier-installer-win:before { 271 | content: "\f17a"; 272 | } 273 | 274 | .fa.classifier-dev:before { 275 | content: "\f079"; 276 | } 277 | 278 | .fa.classifier-sources:before { 279 | content: "\f121"; 280 | } 281 | 282 | .fa.classifier-src:before { 283 | content: "\f121"; 284 | } 285 | 286 | .fa.classifier-changelog:before { 287 | content: "\f15c"; 288 | } 289 | 290 | .fa.classifier-javadoc:before { 291 | content: "\f0f4"; 292 | } 293 | 294 | .fa.classifier-universal:before, .fa.classifier-launcher:before { 295 | content: "\f1c6"; 296 | } 297 | 298 | .fa.classifier-mdk:before { 299 | content: "\f126"; 300 | } 301 | 302 | .fa.classifier-userdev:before { 303 | content: "\f007"; 304 | } 305 | 306 | .fa.classifier-link:before { 307 | content: "\f0c1"; 308 | } 309 | 310 | .promo-latest:before { 311 | content: "\f123"; // Half Star (outlined): fa-star-half-o 312 | } 313 | 314 | .promo-recommended:before { 315 | content: "\f005"; // Full Star 316 | } 317 | 318 | .marker-branch:before { 319 | content: "\f126"; // Fork 320 | } 321 | 322 | .promo-downloads-top, .ad-downloads-top { 323 | margin-bottom: 2rem !important; 324 | min-height: 121px; // significantly reduce the top ad making the rest of the page jump about during page load 325 | } 326 | 327 | .promo-downloads-bottom, .ad-downloads-bottom { 328 | margin-top: 2rem !important; 329 | margin-bottom: 2rem !important; 330 | } 331 | 332 | .info-link { 333 | cursor: pointer; 334 | } 335 | 336 | .info-tooltip { 337 | display: none; 338 | } 339 | 340 | @media screen and (max-width: 1440px) { 341 | .sidebar-wrapper { 342 | .sidebar-left.sidebar-sticky, .sidebar-sticky aside { 343 | min-width: 160px; 344 | max-width: 160px; 345 | } 346 | 347 | .sidebar-right { 348 | min-width: 190px; 349 | max-width: 190px; 350 | } 351 | } 352 | 353 | .sidebar-wrapper .sidebar-sticky-wrapper-content { 354 | max-width: calc(100% - 160px - 1rem); 355 | } 356 | 357 | .promo-downloads-right .promo, .ad-downloads-right .ad { 358 | min-width: 160px; 359 | width: 160px; 360 | max-width: 160px; 361 | height: 600px; 362 | } 363 | } 364 | 365 | @media screen and (max-width: 1170px) { 366 | .downloads { 367 | flex-direction: column; 368 | align-items: center; 369 | 370 | div.download { 371 | margin: 0 0 1rem; 372 | 373 | &:last-child { 374 | margin-bottom: 0; 375 | } 376 | } 377 | } 378 | 379 | .promo-downloads-top .promo, .promo-downloads-bottom .promo, 380 | .ad-downloads-top .ad, .ad-downloads-bottom .ad { 381 | min-width: 320px; 382 | width: 320px; 383 | max-width: 320px; 384 | height: 100px; 385 | margin-left: auto; 386 | margin-right: auto; 387 | } 388 | 389 | .download-list .download-links { 390 | flex-wrap: wrap; 391 | } 392 | } 393 | 394 | @media screen and (max-width: $mobile-break) { 395 | .promos-wrapper { 396 | flex-direction: column-reverse; 397 | 398 | .promo-downloads-top, .ad-downloads-top { 399 | margin: 3rem auto !important; 400 | } 401 | } 402 | 403 | .sidebar-wrapper { 404 | .sidebar-left.sidebar-sticky, .sidebar-sticky aside { 405 | min-width: 0; 406 | max-width: 100%; 407 | } 408 | 409 | .sidebar-sticky-wrapper-content, .sidebar-wrapper-content { 410 | max-width: 100%; 411 | } 412 | } 413 | 414 | .sidebar-wrapper .sidebar-right { 415 | width: 100%; 416 | display: flex; 417 | align-items: center; 418 | flex-direction: column; 419 | margin-left: 0; 420 | max-width: 100%; 421 | } 422 | } 423 | 424 | @media screen and (max-width: 960px) { 425 | .download-list { 426 | td:nth-child(2), th:nth-child(2) { 427 | display: none; 428 | } 429 | 430 | .download-version { 431 | width: auto; 432 | } 433 | 434 | .download-files { 435 | width: auto; 436 | } 437 | } 438 | } 439 | 440 | @media screen and (max-width: 768px) { 441 | .download-list { 442 | display: flex; 443 | flex-direction: column; 444 | align-items: stretch; 445 | 446 | thead { 447 | display: none; 448 | } 449 | 450 | thead.mobile-only { 451 | display: block !important; 452 | } 453 | 454 | tr { 455 | display: flex; 456 | flex-direction: column; 457 | align-items: stretch; 458 | 459 | &:last-child { 460 | td:first-child { 461 | border-radius: 0; 462 | } 463 | 464 | td:last-child { 465 | border-radius: 0 0 3px 3px; 466 | } 467 | } 468 | } 469 | 470 | td, th { 471 | display: flex; 472 | justify-content: center; 473 | align-items: center; 474 | } 475 | 476 | td:first-child { 477 | padding-bottom: 0; 478 | border-right: none; 479 | } 480 | } 481 | } 482 | 483 | @media screen and (max-width: 640px) { 484 | .download-list .download-links { 485 | flex-direction: column; 486 | align-items: center; 487 | 488 | li { 489 | border-right: none; 490 | } 491 | } 492 | } 493 | 494 | @media screen and (max-width: 767px) { 495 | .promo-downloads-right .promo, .ad-downloads-right .ad { 496 | min-width: 300px; 497 | width: 300px; 498 | max-width: 300px; 499 | height: 250px; 500 | margin-left: auto; 501 | margin-right: auto; 502 | } 503 | } 504 | -------------------------------------------------------------------------------- /sass/_forum_screens.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Style adjustments required to make the forums look good on any system. 3 | */ 4 | @media screen and (max-width: 960px) { 5 | .forum-row { 6 | .forum-icon-wrapper { 7 | padding: 0.5rem; 8 | } 9 | 10 | .forum-statistics { 11 | display: none; 12 | } 13 | } 14 | 15 | .heading-statistics { 16 | display: none; 17 | } 18 | 19 | .thread-row { 20 | .thread-statistics { 21 | display: none; 22 | } 23 | } 24 | 25 | html[dir="ltr"] .ipsDataItem_modCheck { 26 | display: flex; 27 | left: initial; 28 | position: relative; 29 | top: initial; 30 | padding: 12px 10px; 31 | justify-content: center; 32 | } 33 | } 34 | 35 | @media screen and (max-width: 767px) { 36 | .thread-reply-body-metadata { 37 | flex-direction: column; 38 | } 39 | 40 | .reply-actions { 41 | flex-wrap: wrap; 42 | margin-left: 0; 43 | } 44 | 45 | html[dir="ltr"] .cForumQuestions .cForumQuestion .ipsDataItem_modCheck, html[dir="ltr"] .cForumQuestions .cForumQuestion .ipsDataItem_main + .cForumQuestion_stat { 46 | margin-left: 0; 47 | } 48 | 49 | .ipsStream { 50 | margin-top: 0; 51 | } 52 | } 53 | 54 | @media screen and (max-width: 679px) { 55 | .thread-list-actions { 56 | flex-direction: column; 57 | justify-content: center; 58 | align-items: stretch; 59 | 60 | .thread-list-buttons { 61 | flex-direction: column; 62 | align-items: stretch; 63 | margin-left: 0; 64 | 65 | li { 66 | margin-left: 0; 67 | margin-bottom: 0.5rem; 68 | 69 | &:last-child { 70 | margin-bottom: 0; 71 | } 72 | } 73 | 74 | a { 75 | padding: 0.5rem 1rem; 76 | text-align: center; 77 | } 78 | } 79 | } 80 | 81 | section.thread-replies { 82 | article.thread-reply { 83 | flex-direction: column; 84 | align-items: center; 85 | padding: 1rem; 86 | 87 | .thread-reply-author { 88 | flex-direction: row; 89 | width: auto; 90 | margin-bottom: 1rem; 91 | 92 | .profile-picture { 93 | margin-bottom: 0; 94 | width: 90px; 95 | height: 90px; 96 | } 97 | 98 | .user-details { 99 | margin-left: 1rem; 100 | } 101 | } 102 | 103 | .thread-reply-body { 104 | margin-left: 0; 105 | } 106 | } 107 | } 108 | 109 | html[dir="ltr"] .ipsDataItem_modCheck { 110 | padding: 0.5rem; 111 | justify-content: flex-end; 112 | background: none; 113 | border-left: none; 114 | border-top: 1px solid $section-highlight; 115 | } 116 | 117 | .forum-row { 118 | flex-direction: column; 119 | align-items: stretch; 120 | margin-bottom: 0.5rem; 121 | 122 | .forum-icon-wrapper { 123 | display: none; 124 | } 125 | 126 | .forum-icon-mobile { 127 | display: inline-block; 128 | } 129 | 130 | &:first-child { 131 | .forum-info { 132 | border-top: none; 133 | } 134 | } 135 | 136 | &:last-child { 137 | margin-bottom: 0; 138 | } 139 | 140 | .forum-info, .forum-statistics, .forum-last-post { 141 | flex-basis: auto; 142 | border-left: none; 143 | } 144 | 145 | .forum-info { 146 | align-items: stretch; 147 | border-top: 1px solid $section-highlight; 148 | 149 | .forum-details { 150 | align-items: center; 151 | } 152 | 153 | .forum-children { 154 | display: none; 155 | } 156 | } 157 | 158 | .forum-last-post { 159 | align-items: stretch; 160 | border-top: 1px solid $section-highlight; 161 | } 162 | 163 | .forum-last-post-fact { 164 | align-items: flex-start; 165 | } 166 | } 167 | 168 | section.thread-list { 169 | .heading { 170 | display: none; 171 | } 172 | } 173 | 174 | .thread-row { 175 | flex-direction: column; 176 | align-items: stretch; 177 | margin-bottom: 0.5rem; 178 | 179 | .thread-icon-wrapper { 180 | display: none; 181 | } 182 | 183 | &:first-child { 184 | border-top-left-radius: 3px; 185 | border-top-right-radius: 3px; 186 | 187 | .thread-info { 188 | border-top: none; 189 | } 190 | } 191 | 192 | &:last-child { 193 | margin-bottom: 0; 194 | } 195 | 196 | .thread-info, .thread-statistics, .thread-last-post { 197 | flex-basis: auto; 198 | border-left: none; 199 | } 200 | 201 | .thread-info { 202 | align-items: stretch; 203 | border-top: 1px solid $section-highlight; 204 | 205 | .thread-details { 206 | align-items: center; 207 | } 208 | } 209 | 210 | .thread-last-post { 211 | align-items: stretch; 212 | border-top: 1px solid $section-highlight; 213 | } 214 | 215 | .thread-last-post-fact { 216 | align-items: flex-start; 217 | } 218 | } 219 | } -------------------------------------------------------------------------------- /sass/_highlighting_dark.scss: -------------------------------------------------------------------------------- 1 | .hljs-strong, 2 | .hljs-emphasis { 3 | color: #a8a8a2; 4 | } 5 | 6 | .hljs-bullet, 7 | .hljs-quote, 8 | .hljs-link, 9 | .hljs-number, 10 | .hljs-regexp, 11 | .hljs-literal { 12 | color: #6896ba; 13 | } 14 | 15 | .hljs-code, 16 | .hljs-selector-class { 17 | color: #a6e22e; 18 | } 19 | 20 | .hljs-emphasis { 21 | font-style: italic; 22 | } 23 | 24 | .hljs-keyword, 25 | .hljs-selector-tag, 26 | .hljs-section, 27 | .hljs-attribute, 28 | .hljs-name, 29 | .hljs-variable { 30 | color: #cb7832; 31 | } 32 | 33 | .hljs-params { 34 | color: #b9b9b9; 35 | } 36 | 37 | .hljs-string { 38 | color: #6a8759; 39 | } 40 | 41 | .hljs-subst, 42 | .hljs-type, 43 | .hljs-built_in, 44 | .hljs-builtin-name, 45 | .hljs-symbol, 46 | .hljs-selector-id, 47 | .hljs-selector-attr, 48 | .hljs-selector-pseudo, 49 | .hljs-template-tag, 50 | .hljs-template-variable, 51 | .hljs-addition { 52 | color: #e0c46c; 53 | } 54 | 55 | .hljs-comment, 56 | .hljs-deletion, 57 | .hljs-meta { 58 | color: #7f7f7f; 59 | } -------------------------------------------------------------------------------- /sass/_highlighting_light.scss: -------------------------------------------------------------------------------- 1 | .hljs-subst, 2 | .hljs-title { 3 | font-weight: normal; 4 | color: #000; 5 | } 6 | 7 | .hljs-comment, 8 | .hljs-quote { 9 | color: #808080; 10 | font-style: italic; 11 | } 12 | 13 | .hljs-meta { 14 | color: #808000; 15 | } 16 | 17 | .hljs-tag { 18 | background: #efefef; 19 | } 20 | 21 | .hljs-section, 22 | .hljs-name, 23 | .hljs-literal, 24 | .hljs-keyword, 25 | .hljs-selector-tag, 26 | .hljs-type, 27 | .hljs-selector-id, 28 | .hljs-selector-class { 29 | font-weight: bold; 30 | color: #000080; 31 | } 32 | 33 | .hljs-attribute, 34 | .hljs-number, 35 | .hljs-regexp, 36 | .hljs-link { 37 | font-weight: bold; 38 | color: #0000ff; 39 | } 40 | 41 | .hljs-number, 42 | .hljs-regexp, 43 | .hljs-link { 44 | font-weight: normal; 45 | } 46 | 47 | .hljs-string { 48 | color: #008000; 49 | font-weight: bold; 50 | } 51 | 52 | .hljs-symbol, 53 | .hljs-bullet, 54 | .hljs-formula { 55 | color: #000; 56 | background: #d0eded; 57 | font-style: italic; 58 | } 59 | 60 | .hljs-doctag { 61 | text-decoration: underline; 62 | } 63 | 64 | .hljs-variable, 65 | .hljs-template-variable { 66 | color: #660e7a; 67 | } 68 | 69 | .hljs-addition { 70 | background: #baeeba; 71 | } 72 | 73 | .hljs-deletion { 74 | background: #ffc8bd; 75 | } 76 | 77 | .hljs-emphasis { 78 | font-style: italic; 79 | } 80 | 81 | .hljs-strong { 82 | font-weight: bold; 83 | } -------------------------------------------------------------------------------- /sass/_layout.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Basic layout shared across all Forge sites. 3 | Allows for easy and consistent looks everywhere. 4 | */ 5 | 6 | // Basic wrapper around all content areas, specifies a centered, fixed width area on desktops 7 | .wrapper { 8 | position: relative; 9 | width: 1720px; 10 | margin: 0 auto; 11 | box-sizing: border-box; 12 | padding: 0 1rem; 13 | } 14 | 15 | // Simple class to hide any element 16 | .hidden { 17 | display: none !important; 18 | } 19 | 20 | // Header area, contains things such as the logo, links, a search area and the user panel 21 | header { 22 | color: #f9f7f7; 23 | background: $header-bg; 24 | margin-bottom: 1rem; 25 | 26 | .wrapper { 27 | display: flex; 28 | align-items: center; 29 | padding: 1.5rem 1rem; 30 | } 31 | 32 | // Generic search field that stretches the header 33 | .search { 34 | margin-left: 3rem; 35 | background: lighten($header-bg, 10%); 36 | height: 40px; 37 | flex-grow: 2; 38 | 39 | .search-field { 40 | position: relative; 41 | height: 100%; 42 | overflow: hidden; 43 | flex-grow: 2; 44 | } 45 | 46 | form { 47 | height: 100%; 48 | display: flex; 49 | align-items: center; 50 | } 51 | 52 | input[type=search] { 53 | box-sizing: border-box; 54 | color: #f9f7f7; 55 | width: 100%; 56 | height: 100%; 57 | background: none; 58 | outline: none; 59 | border: none; 60 | padding: 0 0 0 1rem; 61 | max-width: 100%; 62 | border-radius: 0; 63 | 64 | &:focus { 65 | border-radius: 0; 66 | box-shadow: none; 67 | outline: none; 68 | background: rgba(255, 255, 255, 0.1); 69 | } 70 | } 71 | 72 | input[type=submit], button[type=submit] { 73 | position: relative; 74 | float: right; 75 | box-sizing: border-box; 76 | padding: 0 1rem; 77 | max-width: 52px; 78 | height: 100%; 79 | background: none; 80 | outline: none; 81 | border: none; 82 | color: $accent2; 83 | display: block; 84 | font-family: FontAwesome, sans-serif; 85 | font-size: 1.45rem; 86 | transition: color 0.1s ease-in-out; 87 | 88 | &:hover, &:active { 89 | color: darken($accent2, 5%); 90 | cursor: pointer; 91 | } 92 | } 93 | } 94 | 95 | // Container for user-related data 96 | .user-panel { 97 | display: flex; 98 | align-items: center; 99 | margin-left: auto; 100 | padding-left: 1rem; 101 | 102 | ul.user-panel-links { 103 | display: flex; 104 | list-style-type: none; 105 | align-items: center; 106 | margin-bottom: 0; 107 | 108 | li { 109 | margin-right: 1rem; 110 | 111 | &:last-child { 112 | margin-right: 0; 113 | } 114 | } 115 | } 116 | } 117 | 118 | a { 119 | color: #f9f7f7; 120 | text-decoration: none; 121 | transition: color 0.1s ease-in-out; 122 | 123 | &:hover, &:active, &:focus { 124 | color: darken(#f9f7f7, 10%); 125 | } 126 | } 127 | } 128 | 129 | // The main navigation uses the semantic 'nav' tag around a list of links in order to use the full potential of HTML5 130 | nav { 131 | font-size: 1.2rem; 132 | margin-left: 1rem; 133 | 134 | // Link list is simple and horizontally aligned 135 | .links { 136 | list-style-type: none; 137 | margin: 0; 138 | padding: 0; 139 | 140 | li { 141 | float: left; 142 | padding: 0 0.4rem; 143 | 144 | &:first-child { 145 | padding-left: 0; 146 | } 147 | 148 | &:last-child { 149 | padding-right: 0; 150 | } 151 | } 152 | } 153 | } 154 | 155 | .hero { 156 | margin-top: -1rem; 157 | color: #f9f7f7; 158 | background: darken($header-bg, 5%); 159 | 160 | .wrapper { 161 | padding: 2rem; 162 | } 163 | } 164 | 165 | // Sections are individual blocks separated from each other and the background 166 | section, table { 167 | background: $section-bg; 168 | border-radius: 3px; 169 | box-shadow: 0 1px 2px $section-highlight; 170 | margin-bottom: 1rem; 171 | width: 100%; 172 | 173 | h2, thead, .heading { 174 | font-size: 1.2rem; 175 | color: $pagination-color; 176 | margin: 0; 177 | border-radius: 3px 3px 0 0; 178 | font-weight: normal; 179 | } 180 | 181 | h2, .heading { 182 | background: $accent1; 183 | padding: 0.5rem 1rem; 184 | } 185 | 186 | .section-content { 187 | padding: 0.5rem 1rem; 188 | border-radius: 0 0 3px 3px; 189 | overflow-wrap: break-word; 190 | word-wrap: break-word; 191 | word-break: break-all; 192 | word-break: break-word; 193 | hyphens: auto; 194 | 195 | & > *:last-child { 196 | margin-bottom: 0; 197 | } 198 | } 199 | } 200 | 201 | footer { 202 | text-align: center; 203 | /* margin-top: 2rem; We already add padding */ 204 | border-top: 1px solid rgba(0, 0, 0, 0.1); 205 | padding: 1.5rem 0 0 0; 206 | } 207 | 208 | /* 209 | Several basic controls follow... 210 | */ 211 | 212 | // List sections are vertical listings of entries 213 | .list-section { 214 | padding: 0 !important; 215 | 216 | .row { 217 | position: relative; 218 | display: flex; 219 | flex-flow: row; 220 | align-items: stretch; 221 | height: auto; 222 | border-bottom: 1px solid $section-highlight; 223 | 224 | &:last-child { 225 | border-bottom: none; 226 | } 227 | 228 | .row-entry { 229 | padding: 0.5rem 1rem; 230 | } 231 | } 232 | } 233 | 234 | // Generic table styles, should work everywhere 235 | table { 236 | thead { 237 | th { 238 | background: $accent1; 239 | padding: 0.5rem 1rem; 240 | font-weight: normal; 241 | 242 | &:first-child { 243 | border-top-left-radius: 3px; 244 | } 245 | 246 | &:last-child { 247 | border-top-right-radius: 3px; 248 | } 249 | } 250 | } 251 | 252 | tbody { 253 | background: $section-bg; 254 | 255 | tr:nth-child(2n) td { 256 | background: rgba(0, 0, 0, 0.05); 257 | } 258 | 259 | tr:last-child { 260 | td:first-child { 261 | border-bottom-left-radius: 3px; 262 | } 263 | 264 | td:last-child { 265 | border-bottom-right-radius: 3px; 266 | } 267 | } 268 | 269 | td { 270 | padding: 0.5rem 1rem; 271 | font-weight: normal; 272 | } 273 | } 274 | } 275 | 276 | // Paragraphs receive a slight bottom margin to not stretch content areas too much vertically 277 | p { 278 | margin-bottom: 0.1rem; 279 | } 280 | 281 | // Buttons 282 | .btn { 283 | display: inline-block; 284 | border: none; 285 | padding: 0.5rem 0.7rem; 286 | transition: background 0.2s ease-in-out; 287 | outline: none; 288 | border-radius: 0; 289 | background: $accent2; 290 | color: #fafafa; 291 | 292 | &:hover, &:active, &:focus { 293 | border: none; 294 | color: #fafafa; 295 | background: lighten($accent2, 10%); 296 | } 297 | } 298 | 299 | .btn-large { 300 | padding: 0.7rem 0.9rem; 301 | font-size: 1.2rem; 302 | } 303 | 304 | // General purpose pagination control 305 | .pagination { 306 | display: flex; 307 | align-items: center; 308 | padding: 0; 309 | list-style-type: none; 310 | margin: 0 0 1rem; 311 | 312 | &.pagination-center { 313 | justify-content: center; 314 | } 315 | 316 | li { 317 | display: block; 318 | margin-left: 0.25rem; 319 | 320 | a { 321 | display: block; 322 | padding: 0.2rem 0.5rem; 323 | background: $pagination-bg; 324 | color: $pagination-color; 325 | border-radius: 3px; 326 | transition: background 0.2s ease-in-out; 327 | 328 | &:hover, &:focus, &:active { 329 | color: $pagination-color; 330 | background: darken($pagination-bg, 10%) !important; 331 | } 332 | } 333 | 334 | &.pagination-active { 335 | display: block; 336 | padding: 0.2rem 0.5rem; 337 | background: $pagination-color; 338 | border-radius: 3px; 339 | font-weight: bold; 340 | color: $body-color !important; 341 | box-shadow: 0 2px 2px darken($pagination-color, 10%); 342 | } 343 | 344 | &.pagination-ellipsis { 345 | font-size: 0.9em; 346 | } 347 | 348 | &.pagination-disabled { 349 | display: block; 350 | padding: 0.2rem 0.5rem; 351 | border-radius: 3px; 352 | background: $pagination-bg; 353 | color: $pagination-color; 354 | } 355 | 356 | &.pagination-first, &.pagination-last, &.pagination-next, &.pagination-prev { 357 | a { 358 | background: none; 359 | color: $pagination-bg; 360 | transition: color 0.2s ease-in-out; 361 | 362 | &:hover, &:active, &:focus { 363 | background: none !important; 364 | color: darken($pagination-bg, 5%) !important;; 365 | } 366 | } 367 | 368 | &.pagination-disabled { 369 | background: none; 370 | padding: 0.2rem 0.5rem; 371 | color: $pagination-bg; 372 | } 373 | } 374 | 375 | &:first-child { 376 | padding-left: 0; 377 | margin-left: 0; 378 | 379 | a { 380 | padding-left: 0; 381 | } 382 | } 383 | 384 | &:last-child { 385 | padding-right: 0; 386 | 387 | a { 388 | padding-right: 0; 389 | } 390 | } 391 | } 392 | } 393 | 394 | // Generic code styles 395 | .codeheader { 396 | margin-bottom: 0.5rem; 397 | } 398 | 399 | pre, .ipsCode, code { 400 | font-family: "Source Code Pro", Consolas, monospace; 401 | margin: 0; 402 | white-space: pre; 403 | overflow-wrap: normal; 404 | word-wrap: normal; 405 | font-size: $font-size-code; 406 | background: $section-content-bg; 407 | border: 1px solid darken($section-content-bg, 5%) !important; 408 | border-radius: 3px; 409 | max-width: 100%; 410 | } 411 | 412 | code { 413 | color: lighten($accent4, 5%); 414 | padding: 0.02rem 0.2rem; 415 | overflow: auto; 416 | } 417 | 418 | pre, .ipsCode { 419 | padding: 0.5rem; 420 | color: $body-color; 421 | 422 | code { 423 | display: block; 424 | color: $body-color; 425 | background: none; 426 | padding: 0; 427 | border-radius: 0; 428 | border: none !important; 429 | } 430 | } 431 | 432 | a pre, a code { 433 | color: inherit; 434 | } 435 | 436 | // Block quotes for quoting people 437 | blockquote { 438 | border: none; 439 | border-left: 5px solid darken($body-bg, 3%); 440 | background: lighten($body-bg, 3%); 441 | padding: 0.5rem; 442 | 443 | footer { 444 | text-align: left; 445 | padding: 0; 446 | font-size: 0.8rem; 447 | 448 | &:before { 449 | content: '- '; 450 | } 451 | } 452 | } 453 | 454 | // Theme switch 455 | .theme-switch-wrapper { 456 | position: relative; 457 | display: flex; 458 | align-items: center; 459 | justify-content: center; 460 | } 461 | 462 | .theme-switch { 463 | position: relative; 464 | display: inline-block; 465 | width: 3.25rem; 466 | height: 1.5rem; 467 | margin-left: 0.25rem; 468 | margin-bottom: 0; 469 | 470 | input { 471 | display: none; 472 | } 473 | 474 | /* The slider */ 475 | .slider { 476 | position: absolute; 477 | cursor: pointer; 478 | top: 0; 479 | left: 0; 480 | right: 0; 481 | bottom: 0; 482 | background-color: $text-muted; 483 | transition: .4s; 484 | border-radius: 20px; 485 | } 486 | 487 | .slider:before { 488 | position: absolute; 489 | content: ''; 490 | height: 1rem; 491 | width: 1rem; 492 | left: 0.25rem; 493 | bottom: 0.25rem; 494 | background-color: #fff; 495 | transition: .4s; 496 | border-radius: 20px; 497 | } 498 | 499 | input:checked + .slider { 500 | background-color: $accent2; 501 | } 502 | 503 | input:focus + .slider { 504 | box-shadow: 0 0 1px $accent2; 505 | } 506 | 507 | input:checked + .slider:before { 508 | transform: translateX(1.75rem); 509 | } 510 | } 511 | 512 | // Import adjustments for non-desktop devices 513 | @import "layout_screens"; 514 | -------------------------------------------------------------------------------- /sass/_layout_screens.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Style adjustments required to make the generic layout look good on any system. 3 | */ 4 | // Mobile only elements 5 | .mobile-only { 6 | display: none; 7 | } 8 | 9 | @media screen and (max-width: 1720px) { 10 | .wrapper { 11 | width: 100%; 12 | } 13 | } 14 | 15 | @media screen and (max-width: $mobile-break) { 16 | header { 17 | .wrapper { 18 | flex-direction: column; 19 | } 20 | 21 | nav, .search, .user-panel { 22 | margin-left: 0; 23 | } 24 | 25 | .user-panel { 26 | padding-left: 0; 27 | } 28 | 29 | nav { 30 | margin-top: 0.2rem; 31 | } 32 | 33 | .search, .user-panel { 34 | margin-top: 0.7rem; 35 | } 36 | 37 | .search { 38 | flex-grow: 1; 39 | align-self: stretch; 40 | } 41 | } 42 | } 43 | 44 | @media screen and (max-width: 679px) { 45 | .mobile-only { 46 | display: block; 47 | } 48 | 49 | header { 50 | .user-panel { 51 | flex-direction: column; 52 | 53 | .user-panel-links { 54 | margin-left: 0; 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /sass/_normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Prevent adjustments of font size after orientation changes in IE and iOS. 6 | */ 7 | 8 | html { 9 | font-family: sans-serif; /* 1 */ 10 | -ms-text-size-adjust: 100%; /* 2 */ 11 | -webkit-text-size-adjust: 100%; /* 2 */ 12 | } 13 | 14 | /** 15 | * Remove the margin in all browsers (opinionated). 16 | */ 17 | 18 | body { 19 | margin: 0; 20 | } 21 | 22 | /* HTML5 display definitions 23 | ========================================================================== */ 24 | 25 | /** 26 | * Add the correct display in IE 9-. 27 | * 1. Add the correct display in Edge, IE, and Firefox. 28 | * 2. Add the correct display in IE. 29 | */ 30 | 31 | article, 32 | aside, 33 | details, /* 1 */ 34 | figcaption, 35 | figure, 36 | footer, 37 | header, 38 | main, /* 2 */ 39 | menu, 40 | nav, 41 | section, 42 | summary { /* 1 */ 43 | display: block; 44 | } 45 | 46 | /** 47 | * Add the correct display in IE 9-. 48 | */ 49 | 50 | audio, 51 | canvas, 52 | progress, 53 | video { 54 | display: inline-block; 55 | } 56 | 57 | /** 58 | * Add the correct display in iOS 4-7. 59 | */ 60 | 61 | audio:not([controls]) { 62 | display: none; 63 | height: 0; 64 | } 65 | 66 | /** 67 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 68 | */ 69 | 70 | progress { 71 | vertical-align: baseline; 72 | } 73 | 74 | /** 75 | * Add the correct display in IE 10-. 76 | * 1. Add the correct display in IE. 77 | */ 78 | 79 | template, /* 1 */ 80 | [hidden] { 81 | display: none; 82 | } 83 | 84 | /* Links 85 | ========================================================================== */ 86 | 87 | /** 88 | * 1. Remove the gray background on active links in IE 10. 89 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 90 | */ 91 | 92 | a { 93 | background-color: transparent; /* 1 */ 94 | -webkit-text-decoration-skip: objects; /* 2 */ 95 | } 96 | 97 | /** 98 | * Remove the outline on focused links when they are also active or hovered 99 | * in all browsers (opinionated). 100 | */ 101 | 102 | a:active, 103 | a:hover { 104 | outline-width: 0; 105 | } 106 | 107 | /* Text-level semantics 108 | ========================================================================== */ 109 | 110 | /** 111 | * 1. Remove the bottom border in Firefox 39-. 112 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: none; /* 1 */ 117 | text-decoration: underline; /* 2 */ 118 | text-decoration: underline dotted; /* 2 */ 119 | } 120 | 121 | /** 122 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 123 | */ 124 | 125 | b, 126 | strong { 127 | font-weight: inherit; 128 | } 129 | 130 | /** 131 | * Add the correct font weight in Chrome, Edge, and Safari. 132 | */ 133 | 134 | b, 135 | strong { 136 | font-weight: bolder; 137 | } 138 | 139 | /** 140 | * Add the correct font style in Android 4.3-. 141 | */ 142 | 143 | dfn { 144 | font-style: italic; 145 | } 146 | 147 | /** 148 | * Correct the font size and margin on `h1` elements within `section` and 149 | * `article` contexts in Chrome, Firefox, and Safari. 150 | */ 151 | 152 | h1 { 153 | font-size: 2em; 154 | margin: 0.67em 0; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Remove the border on images inside links in IE 10-. 200 | */ 201 | 202 | img { 203 | border-style: none; 204 | } 205 | 206 | /** 207 | * Hide the overflow in IE. 208 | */ 209 | 210 | svg:not(:root) { 211 | overflow: hidden; 212 | } 213 | 214 | /* Grouping content 215 | ========================================================================== */ 216 | 217 | /** 218 | * 1. Correct the inheritance and scaling of font size in all browsers. 219 | * 2. Correct the odd `em` font sizing in all browsers. 220 | */ 221 | 222 | code, 223 | kbd, 224 | pre, 225 | samp { 226 | font-family: monospace, monospace; /* 1 */ 227 | font-size: 1em; /* 2 */ 228 | } 229 | 230 | /** 231 | * Add the correct margin in IE 8. 232 | */ 233 | 234 | figure { 235 | margin: 1em 40px; 236 | } 237 | 238 | /** 239 | * 1. Add the correct box sizing in Firefox. 240 | * 2. Show the overflow in Edge and IE. 241 | */ 242 | 243 | hr { 244 | box-sizing: content-box; /* 1 */ 245 | height: 0; /* 1 */ 246 | overflow: visible; /* 2 */ 247 | } 248 | 249 | /* Forms 250 | ========================================================================== */ 251 | 252 | /** 253 | * 1. Change font properties to `inherit` in all browsers (opinionated). 254 | * 2. Remove the margin in Firefox and Safari. 255 | */ 256 | 257 | button, 258 | input, 259 | select, 260 | textarea { 261 | font: inherit; /* 1 */ 262 | margin: 0; /* 2 */ 263 | } 264 | 265 | /** 266 | * Restore the font weight unset by the previous rule. 267 | */ 268 | 269 | optgroup { 270 | font-weight: bold; 271 | } 272 | 273 | /** 274 | * Show the overflow in IE. 275 | * 1. Show the overflow in Edge. 276 | */ 277 | 278 | button, 279 | input { /* 1 */ 280 | overflow: visible; 281 | } 282 | 283 | /** 284 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 285 | * 1. Remove the inheritance of text transform in Firefox. 286 | */ 287 | 288 | button, 289 | select { /* 1 */ 290 | text-transform: none; 291 | } 292 | 293 | /** 294 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 295 | * controls in Android 4. 296 | * 2. Correct the inability to style clickable types in iOS and Safari. 297 | */ 298 | 299 | button, 300 | html [type="button"], /* 1 */ 301 | [type="reset"], 302 | [type="submit"] { 303 | -webkit-appearance: button; /* 2 */ 304 | } 305 | 306 | /** 307 | * Remove the inner border and padding in Firefox. 308 | */ 309 | 310 | button::-moz-focus-inner, 311 | [type="button"]::-moz-focus-inner, 312 | [type="reset"]::-moz-focus-inner, 313 | [type="submit"]::-moz-focus-inner { 314 | border-style: none; 315 | padding: 0; 316 | } 317 | 318 | /** 319 | * Restore the focus styles unset by the previous rule. 320 | */ 321 | 322 | button:-moz-focusring, 323 | [type="button"]:-moz-focusring, 324 | [type="reset"]:-moz-focusring, 325 | [type="submit"]:-moz-focusring { 326 | outline: 1px dotted ButtonText; 327 | } 328 | 329 | /** 330 | * Change the border, margin, and padding in all browsers (opinionated). 331 | */ 332 | 333 | fieldset { 334 | border: 1px solid #c0c0c0; 335 | margin: 0 2px; 336 | padding: 0.35em 0.625em 0.75em; 337 | } 338 | 339 | /** 340 | * 1. Correct the text wrapping in Edge and IE. 341 | * 2. Correct the color inheritance from `fieldset` elements in IE. 342 | * 3. Remove the padding so developers are not caught out when they zero out 343 | * `fieldset` elements in all browsers. 344 | */ 345 | 346 | legend { 347 | box-sizing: border-box; /* 1 */ 348 | color: inherit; /* 2 */ 349 | display: table; /* 1 */ 350 | max-width: 100%; /* 1 */ 351 | padding: 0; /* 3 */ 352 | white-space: normal; /* 1 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * Correct the text style of placeholders in Chrome, Edge, and Safari. 404 | */ 405 | 406 | ::-webkit-input-placeholder { 407 | color: inherit; 408 | opacity: 0.54; 409 | } 410 | 411 | /** 412 | * 1. Correct the inability to style clickable types in iOS and Safari. 413 | * 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; /* 1 */ 418 | font: inherit; /* 2 */ 419 | } -------------------------------------------------------------------------------- /sass/_privacy.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Styles related to privacy-related settings. 3 | */ 4 | 5 | // Bar under the navigation for privacy disclaimer 6 | .privacy-disclaimer { 7 | position: -webkit-sticky; 8 | position: sticky; 9 | display: none; 10 | top: 0; 11 | width: 100%; 12 | margin-top: -1rem; 13 | margin-bottom: 1rem; 14 | 15 | z-index: 1000000; 16 | 17 | background: #2f2f2f; 18 | color: #eeeeee; 19 | 20 | .wrapper { 21 | padding: 2rem 1rem; 22 | display: flex; 23 | flex-wrap: wrap; 24 | align-items: center; 25 | } 26 | 27 | &-text a { 28 | color: lighten($accent6, 30%); 29 | 30 | &:hover, &:active, &:focus { 31 | color: lighten($accent6, 35%); 32 | } 33 | } 34 | 35 | &-links { 36 | margin-left: auto; 37 | 38 | a { 39 | margin-left: 0.5rem; 40 | } 41 | 42 | a.btn-cookie-disclaimer-decline { 43 | color: #eeeeee; 44 | 45 | &:hover, &:active, &:focus { 46 | color: #fafafa; 47 | } 48 | } 49 | } 50 | } 51 | 52 | .privacy-policy { 53 | display: flex; 54 | flex-direction: column; 55 | 56 | .privacy-settings { 57 | margin: 1rem 0; 58 | 59 | a.btn-privacy-settings-decline { 60 | margin-left: 0.5rem; 61 | } 62 | } 63 | 64 | .donation-links { 65 | margin: 1rem 0; 66 | list-style-type: none; 67 | padding-left: 0.5rem; 68 | 69 | li { 70 | margin-bottom: 0.25rem; 71 | } 72 | 73 | i { 74 | text-align: center; 75 | width: 1rem; 76 | margin-right: 0.5rem; 77 | font-size: 1.1rem; 78 | } 79 | } 80 | 81 | .privacy-advertisers-list { 82 | padding-left: 2rem; 83 | column-count: 4; 84 | column-gap: 2.5rem; 85 | margin-bottom: 1rem; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /sass/_reboot.scss: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | // scss-lint:disable ImportantRule, QualifyingElement, DuplicateProperty 4 | 5 | // Reboot 6 | // 7 | // Global resets to common HTML elements and more for easier usage by Bootstrap. 8 | // Adds additional rules on top of Normalize.css, including several overrides. 9 | 10 | // Reset the box-sizing 11 | // 12 | // Change from `box-sizing: content-box` to `border-box` so that when you add 13 | // `padding` or `border`s to an element, the overall declared `width` does not 14 | // change. For example, `width: 100px;` will always be `100px` despite the 15 | // `border: 10px solid red;` and `padding: 20px;`. 16 | // 17 | // Heads up! This reset may cause conflicts with some third-party widgets. For 18 | // recommendations on resolving such conflicts, see 19 | // http://getbootstrap.com/getting-started/#third-box-sizing. 20 | // 21 | // Credit: https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ 22 | 23 | html { 24 | box-sizing: border-box; 25 | width: 100%; 26 | max-width: 100%; 27 | } 28 | 29 | body { 30 | width: 100%; 31 | max-width: 100%; 32 | } 33 | 34 | *, 35 | *::before, 36 | *::after { 37 | box-sizing: inherit; 38 | } 39 | 40 | // Make viewport responsive 41 | // 42 | // @viewport is needed because IE 10+ doesn't honor in 43 | // some cases. See http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/. 44 | // Eventually @viewport will replace . 45 | // 46 | // However, `device-width` is broken on IE 10 on Windows (Phone) 8, 47 | // (see http://timkadlec.com/2013/01/windows-phone-8-and-device-width/ and https://github.com/twbs/bootstrap/issues/10497) 48 | // and the fix for that involves a snippet of JavaScript to sniff the user agent 49 | // and apply some conditional CSS. 50 | // 51 | // See http://getbootstrap.com/getting-started/#support-ie10-width for the relevant hack. 52 | // 53 | // Wrap `@viewport` with `@at-root` for when folks do a nested import (e.g., 54 | // `.class-name { @import "bootstrap"; }`). 55 | @at-root { 56 | @-ms-viewport { 57 | width: device-width; 58 | } 59 | } 60 | 61 | // 62 | // Reset HTML, body, and more 63 | // 64 | 65 | html { 66 | // Sets a specific default `font-size` for user with `rem` type scales. 67 | font-size: $font-size-root; 68 | // As a side-effect of setting the @viewport above, 69 | // IE11 & Edge make the scrollbar overlap the content and automatically hide itself when not in use. 70 | // Unfortunately, the auto-showing of the scrollbar is sometimes too sensitive, 71 | // thus making it hard to click on stuff near the right edge of the page. 72 | // So we add this style to force IE11 & Edge to use a "normal", non-overlapping, non-auto-hiding scrollbar. 73 | // See https://github.com/twbs/bootstrap/issues/18543 74 | -ms-overflow-style: scrollbar; 75 | // Changes the default tap highlight to be completely transparent in iOS. 76 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 77 | } 78 | 79 | body { 80 | // Make the `body` use the `font-size-root` 81 | font-family: $font-family-base; 82 | font-size: $font-size-base; 83 | line-height: $line-height-base; 84 | // Go easy on the eyes and use something other than `#000` for text 85 | color: $body-color; 86 | // By default, `` has no `background-color` so we set one as a best practice. 87 | background-color: $body-bg; 88 | } 89 | 90 | // Suppress the focus outline on elements that cannot be accessed via keyboard. 91 | // This prevents an unwanted focus outline from appearing around elements that 92 | // might still respond to pointer events. 93 | // 94 | // Credit: https://github.com/suitcss/base 95 | [tabindex="-1"]:focus { 96 | outline: none !important; 97 | } 98 | 99 | // 100 | // Typography 101 | // 102 | 103 | // Remove top margins from headings 104 | // 105 | // By default, `

`-`

` all receive top and bottom margins. We nuke the top 106 | // margin for easier control within type scales as it avoids margin collapsing. 107 | h1, h2, h3, h4, h5, h6 { 108 | margin-top: 0; 109 | margin-bottom: .5rem; 110 | } 111 | 112 | // Reset margins on paragraphs 113 | // 114 | // Similarly, the top margin on `

`s get reset. However, we also reset the 115 | // bottom margin to use `rem` units instead of `em`. 116 | p { 117 | margin-top: 0; 118 | margin-bottom: 1rem; 119 | } 120 | 121 | // Abbreviations and acronyms 122 | abbr[title], 123 | // Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 124 | abbr[data-original-title] { 125 | cursor: help; 126 | border-bottom: 1px dotted $abbr-border-color; 127 | } 128 | 129 | address { 130 | margin-bottom: 1rem; 131 | font-style: normal; 132 | line-height: inherit; 133 | } 134 | 135 | ol, 136 | ul, 137 | dl { 138 | margin-top: 0; 139 | margin-bottom: 1rem; 140 | } 141 | 142 | ol ol, 143 | ul ul, 144 | ol ul, 145 | ul ol { 146 | margin-bottom: 0; 147 | } 148 | 149 | dt { 150 | font-weight: $dt-font-weight; 151 | } 152 | 153 | dd { 154 | margin-bottom: .5rem; 155 | margin-left: 0; // Undo browser default 156 | } 157 | 158 | blockquote { 159 | margin: 0 0 1rem; 160 | } 161 | 162 | // 163 | // Links 164 | // 165 | a { 166 | color: $link-color; 167 | text-decoration: $link-decoration; 168 | 169 | &:hover, &:focus { 170 | color: $link-hover-color; 171 | text-decoration: $link-hover-decoration; 172 | } 173 | 174 | &:focus { 175 | // Default 176 | outline: thin dotted; 177 | // WebKit 178 | outline: 5px auto -webkit-focus-ring-color; 179 | outline-offset: -2px; 180 | } 181 | } 182 | 183 | // And undo these styles for placeholder links/named anchors (without href) 184 | // which have not been made explicitly keyboard-focusable (without tabindex). 185 | // It would be more straightforward to just use a[href] in previous block, but that 186 | // causes specificity issues in many other styles that are too complex to fix. 187 | // See https://github.com/twbs/bootstrap/issues/19402 188 | 189 | a:not([href]):not([tabindex]) { 190 | color: inherit; 191 | text-decoration: none; 192 | 193 | &:hover, &:focus { 194 | color: inherit; 195 | text-decoration: none; 196 | } 197 | 198 | &:focus { 199 | outline: none; 200 | } 201 | } 202 | 203 | // 204 | // Code 205 | // 206 | 207 | pre { 208 | // Remove browser default top margin 209 | margin-top: 0; 210 | // Reset browser default of `1em` to use `rem`s 211 | margin-bottom: 1rem; 212 | // Normalize v4 removed this property, causing `

` content to break out of wrapping code snippets
213 |   //overflow: auto;
214 | }
215 | 
216 | //
217 | // Figures
218 | //
219 | 
220 | figure {
221 |   // Normalize adds `margin` to `figure`s as browsers apply it inconsistently.
222 |   // We reset that to create a better flow in-page.
223 |   margin: 0 0 1rem;
224 | }
225 | 
226 | //
227 | // Images
228 | //
229 | 
230 | img {
231 |   // By default, ``s are `inline-block`. This assumes that, and vertically
232 |   // centers them. This won't apply should you reset them to `block` level.
233 |   vertical-align: middle;
234 |   // Note: ``s are deliberately not made responsive by default.
235 |   // For the rationale behind this, see the comments on the `.img-fluid` class.
236 | }
237 | 
238 | // iOS "clickable elements" fix for role="button"
239 | //
240 | // Fixes "clickability" issue (and more generally, the firing of events such as focus as well)
241 | // for traditionally non-focusable elements with role="button"
242 | // see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
243 | 
244 | [role="button"] {
245 |   cursor: pointer;
246 | }
247 | 
248 | // Avoid 300ms click delay on touch devices that support the `touch-action` CSS property.
249 | //
250 | // In particular, unlike most other browsers, IE11+Edge on Windows 10 on touch devices and IE Mobile 10-11
251 | // DON'T remove the click delay when `` is present.
252 | // However, they DO support removing the click delay via `touch-action: manipulation`.
253 | // See:
254 | // * http://v4-alpha.getbootstrap.com/content/reboot/#click-delay-optimization-for-touch
255 | // * http://caniuse.com/#feat=css-touch-action
256 | // * http://patrickhlauke.github.io/touch/tests/results/#suppressing-300ms-delay
257 | 
258 | a,
259 | area,
260 | button,
261 | [role="button"],
262 | input,
263 | label,
264 | select,
265 | summary,
266 | textarea {
267 |   touch-action: manipulation;
268 | }
269 | 
270 | //
271 | // Tables
272 | //
273 | 
274 | table {
275 |   // No longer part of Normalize since v4
276 |   border-collapse: collapse;
277 |   // Reset for nesting within parents with `background-color`.
278 |   background-color: $table-bg;
279 | }
280 | 
281 | caption {
282 |   padding-top: $table-cell-padding;
283 |   padding-bottom: $table-cell-padding;
284 |   color: $text-muted;
285 |   text-align: left;
286 |   caption-side: bottom;
287 | }
288 | 
289 | th {
290 |   // Centered by default, but left-align-ed to match the `td`s below.
291 |   text-align: left;
292 | }
293 | 
294 | //
295 | // Forms
296 | //
297 | 
298 | label {
299 |   // Allow labels to use `margin` for spacing.
300 |   display: inline-block;
301 |   margin-bottom: .5rem;
302 | }
303 | 
304 | // Work around a Firefox/IE bug where the transparent `button` background
305 | // results in a loss of the default `button` focus styles.
306 | //
307 | // Credit: https://github.com/suitcss/base/
308 | button:focus {
309 |   outline: 1px dotted;
310 |   outline: 5px auto -webkit-focus-ring-color;
311 | }
312 | 
313 | input,
314 | button,
315 | select,
316 | textarea {
317 |   // Remove all `margin`s so our classes don't have to do it themselves.
318 |   margin: 0;
319 |   // Normalize includes `font: inherit;`, so `font-family`. `font-size`, etc are
320 |   // properly inherited. However, `line-height` isn't addressed there. Using this
321 |   // ensures we don't need to unnecessarily redeclare the global font stack.
322 |   line-height: inherit;
323 |   // iOS adds rounded borders by default
324 |   border-radius: 0;
325 | }
326 | 
327 | input[type="radio"],
328 | input[type="checkbox"] {
329 |   // Apply a disabled cursor for radios and checkboxes.
330 |   //
331 |   // Note: Neither radios nor checkboxes can be readonly.
332 |   &:disabled {
333 |     cursor: $cursor-disabled;
334 |   }
335 | }
336 | 
337 | input[type="date"],
338 | input[type="time"],
339 | input[type="datetime-local"],
340 | input[type="month"] {
341 |   // Remove the default appearance of temporal inputs to avoid a Mobile Safari
342 |   // bug where setting a custom line-height prevents text from being vertically
343 |   // centered within the input.
344 |   //
345 |   // Bug report: https://github.com/twbs/bootstrap/issues/11266
346 |   -webkit-appearance: listbox;
347 | }
348 | 
349 | textarea {
350 |   // Textareas should really only resize vertically so they don't break their (horizontal) containers.
351 |   resize: vertical;
352 | }
353 | 
354 | fieldset {
355 |   // Chrome and Firefox set a `min-width: min-content;` on fieldsets,
356 |   // so we reset that to ensure it behaves more like a standard block element.
357 |   // See https://github.com/twbs/bootstrap/issues/12359.
358 |   min-width: 0;
359 |   // Reset the default outline behavior of fieldsets so they don't affect page layout.
360 |   padding: 0;
361 |   margin: 0;
362 |   border: 0;
363 | }
364 | 
365 | legend {
366 |   // Reset the entire legend element to match the `fieldset`
367 |   display: block;
368 |   width: 100%;
369 |   padding: 0;
370 |   margin-bottom: .5rem;
371 |   font-size: 1.5rem;
372 |   line-height: inherit;
373 | }
374 | 
375 | input[type="search"] {
376 |   // This overrides the extra rounded corners on search inputs in iOS so that our
377 |   // `.form-control` class can properly style them. Note that this cannot simply
378 |   // be added to `.form-control` as it's not specific enough. For details, see
379 |   // https://github.com/twbs/bootstrap/issues/11586.
380 |   -webkit-appearance: none;
381 | }
382 | 
383 | // todo: needed?
384 | output {
385 |   display: inline-block;
386 |   //  font-size: $font-size-base;
387 |   //  line-height: $line-height;
388 |   //  color: $input-color;
389 | }
390 | 
391 | // Always hide an element with the `hidden` HTML attribute (from PureCSS).
392 | [hidden] {
393 |   display: none !important;
394 | }


--------------------------------------------------------------------------------
/sass/_scrollpane.scss:
--------------------------------------------------------------------------------
 1 | .jspContainer {
 2 |   overflow: hidden;
 3 |   position: relative;
 4 | }
 5 | 
 6 | .jspPane {
 7 |   position: absolute;
 8 |   padding: 0 !important;
 9 | }
10 | 
11 | .jspVerticalBar {
12 |   position: absolute;
13 |   top: 0;
14 |   right: 0;
15 |   width: 8px;
16 |   height: 100%;
17 | }
18 | 
19 | .jspHorizontalBar {
20 |   position: absolute;
21 |   bottom: 0;
22 |   left: 0;
23 |   width: 100%;
24 |   height: 8px;
25 | }
26 | 
27 | .jspCap {
28 |   display: none;
29 | }
30 | 
31 | .jspHorizontalBar .jspCap {
32 |   float: left;
33 | }
34 | 
35 | .jspTrack {
36 |   background: $scrollbar-track-bg;
37 |   position: relative;
38 | }
39 | 
40 | .jspDrag {
41 |   background: $scrollbar-drag-bg;
42 |   position: relative;
43 |   top: 0;
44 |   left: 0;
45 |   cursor: pointer;
46 |   transition: background 0.4s ease-in-out;
47 | 
48 |   &:hover, &:active, &:focus {
49 |     background: $scrollbar-drag-hover-bg;
50 |   }
51 | }
52 | 
53 | .jspHorizontalBar .jspTrack,
54 | .jspHorizontalBar .jspDrag {
55 |   float: left;
56 |   height: 100%;
57 | }
58 | 
59 | .jspArrow {
60 |   background: #50506d;
61 |   text-indent: -20000px;
62 |   display: block;
63 |   cursor: pointer;
64 |   padding: 0;
65 |   margin: 0;
66 | }
67 | 
68 | .jspArrow.jspDisabled {
69 |   cursor: default;
70 |   background: #80808d;
71 | }
72 | 
73 | .jspVerticalBar .jspArrow {
74 |   height: 16px;
75 | }
76 | 
77 | .jspHorizontalBar .jspArrow {
78 |   width: 16px;
79 |   float: left;
80 |   height: 100%;
81 | }
82 | 
83 | .jspVerticalBar .jspArrow:focus {
84 |   outline: none;
85 | }
86 | 
87 | .jspCorner {
88 |   background: #eeeef4;
89 |   float: left;
90 |   height: 100%;
91 | }
92 | 
93 | /* Yuk! CSS Hack for IE6 3 pixel bug :( */
94 | * html .jspCorner {
95 |   margin: 0 -3px 0 0;
96 | }
97 | 


--------------------------------------------------------------------------------
/sass/_sidebars.scss:
--------------------------------------------------------------------------------
  1 | .sidebar-wrapper {
  2 |   position: relative;
  3 |   box-sizing: border-box;
  4 |   display: flex;
  5 | 
  6 |   .sidebar-left {
  7 |     margin-right: 1rem;
  8 |   }
  9 | 
 10 |   .sidebar-right {
 11 |     margin-left: 1rem;
 12 |   }
 13 | 
 14 |   .sidebar-left, .sidebar-right, aside {
 15 |     min-width: 330px;
 16 |     max-width: 330px;
 17 |   }
 18 | 
 19 |   .sidebar-sticky {
 20 |     position: -webkit-sticky;
 21 |     position: sticky;
 22 |     top: 1rem;
 23 |   }
 24 | 
 25 |   .sidebar-sticky, .sidebar-sticky aside {
 26 |     min-width: 330px;
 27 |     max-width: 330px;
 28 |     max-height: calc(100vh - (90px + 2rem));
 29 |   }
 30 | 
 31 |   .sidebar-wrapper-content, .sidebar-sticky-wrapper-content {
 32 |     flex-grow: 1;
 33 |   }
 34 | 
 35 |   .sidebar-sticky-wrapper-content {
 36 |     min-height: calc(100vh - 182px); /* 182px, a good size for the footer, Doesn't scroll on anything >= 900 */
 37 |     max-width: calc(100% - 330px - 1rem);
 38 |   }
 39 | }
 40 | 
 41 | .open-sidebar, .close-sidebar {
 42 |   display: none;
 43 | }
 44 | 
 45 | section.sidebar-nav h2 {
 46 |   border-bottom: none;
 47 | }
 48 | 
 49 | section.sidebar-nav {
 50 |   background: none;
 51 |   box-shadow: none;
 52 | }
 53 | 
 54 | .collapsible-icon {
 55 |   font-size: 0.8rem !important;
 56 |   margin-right: 0.25rem;
 57 | }
 58 | 
 59 | section.sidebar-nav > ul {
 60 |   list-style-type: none;
 61 |   margin: 0;
 62 |   padding: 0;
 63 |   max-height: calc(100vh - (90px + 42px + 2rem));
 64 |   overflow-y: scroll;
 65 | 
 66 |   .elem-text {
 67 |     display: block;
 68 |   }
 69 | 
 70 |   .elem-active {
 71 |     font-weight: bold;
 72 | 
 73 |     & > * {
 74 |       font-weight: normal;
 75 |     }
 76 | 
 77 |     & > .elem-text {
 78 |       font-weight: bold;
 79 |     }
 80 |   }
 81 | 
 82 |   .jspPane {
 83 |     background: $section-bg;
 84 |     border-radius: 0 0 3px 3px;
 85 |     box-shadow: 0 1px 2px $section-highlight;
 86 |   }
 87 | 
 88 |   & > li, .jspPane > li {
 89 |     & > .elem-text {
 90 |       display: block;
 91 |       line-height: 1rem;
 92 |       color: $pagination-color;
 93 |       background: lighten($accent1, 5%);
 94 |       padding: 0.5rem;
 95 |     }
 96 | 
 97 |     & > a.elem-text {
 98 |       transition: background 0.2s ease-in-out;
 99 | 
100 |       &:hover, &:active, &:focus {
101 |         background: lighten($accent1, 10%);
102 |       }
103 |     }
104 | 
105 |     ul {
106 |       list-style-type: none;
107 |       background: $section-bg;
108 |       margin: 0;
109 |       padding: 0;
110 | 
111 |       li {
112 |         padding: 0.2rem 0.5rem;
113 |       }
114 |     }
115 |   }
116 | 
117 |   a {
118 |     display: block;
119 |   }
120 | }
121 | 
122 | @media screen and (max-width: $mobile-break) {
123 |   .logo {
124 |     width: 100%;
125 |     text-align: center;
126 |     display: flex;
127 |     justify-content: center;
128 |     align-items: center;
129 | 
130 |     .logo-image {
131 |       margin: 0 auto;
132 |     }
133 |   }
134 | 
135 |   body.sidebar-active {
136 |     position: fixed;
137 |     width: 100%;
138 |   }
139 | 
140 |   .open-sidebar {
141 |     display: block;
142 |     font-size: 2rem;
143 |   }
144 | 
145 |   .close-sidebar {
146 |     display: inline;
147 |     color: $pagination-color;
148 |     font-size: 1.5rem;
149 |     margin-right: 0.5rem;
150 | 
151 |     &:hover, &:active {
152 |       color: darken($pagination-color, 5%);
153 |     }
154 |   }
155 | 
156 |   .sidebar-wrapper {
157 |     flex-direction: column;
158 | 
159 |     &-content, .sidebar-sticky-wrapper-content {
160 |       max-width: 100%;
161 |       min-height: 0;
162 |     }
163 | 
164 |     .sidebar-left.sidebar-sticky {
165 |       transform: translateX(-100%);
166 |     }
167 | 
168 |     .sidebar-right.sidebar-sticky {
169 |       transform: translateX(100%);
170 |     }
171 | 
172 |     & > .sidebar-left, & > .sidebar-right, & > aside, & > .sidebar-left > aside, & > .sidebar-right > aside {
173 |       width: 100%;
174 |       max-width: 100%;
175 |       display: flex;
176 |       align-items: center;
177 |       flex-direction: column;
178 |     }
179 | 
180 |     .sidebar-left {
181 |       margin-right: 0;
182 |     }
183 | 
184 |     .sidebar-right {
185 |       margin-left: 0;
186 |     }
187 | 
188 |     .sidebar-sticky {
189 |       position: fixed;
190 |       top: 0;
191 |       left: 0;
192 |       z-index: 10000;
193 |       width: 100%;
194 |       height: 100%;
195 |       transition: transform 0.75s ease-in-out;
196 |       max-width: 100%;
197 |       max-height: 100%;
198 | 
199 |       &.active-sidebar {
200 |         transform: translateX(0);
201 |       }
202 | 
203 |       aside, section {
204 |         height: 100%;
205 |         box-shadow: none;
206 |         max-width: 100%;
207 |         max-height: 100%;
208 |       }
209 | 
210 |       section.sidebar-nav {
211 |         background: $section-bg;
212 |       }
213 | 
214 |       section.sidebar-nav > ul {
215 |         max-height: calc(100% - 42px);
216 | 
217 |         .jspPane {
218 |           border-radius: 0;
219 |           box-shadow: none;
220 |         }
221 |       }
222 | 
223 |       section {
224 |         &, h2, .heading {
225 |           border-radius: 0;
226 |         }
227 | 
228 |         h2, .heading {
229 |           display: flex;
230 |           align-items: center;
231 |         }
232 |       }
233 |     }
234 |   }
235 | }
236 | 


--------------------------------------------------------------------------------
/sass/_styles.scss:
--------------------------------------------------------------------------------
 1 | /*
 2 |  Generic stylesheet shared across all platforms
 3 |  */
 4 | @import "base";
 5 | @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300);
 6 | @import url('https://fonts.googleapis.com/css?family=Source+Code+Pro:300,400,700&subset=latin-ext');
 7 | 
 8 | @import "layout";
 9 | // All sites may contain ads
10 | @import "ads";
11 | 
12 | .clearfix {
13 |   &:after {
14 |     content: ".";
15 |     clear: both;
16 |     display: block;
17 |     visibility: hidden;
18 |     height: 0;
19 |   }
20 | }


--------------------------------------------------------------------------------
/sass/_styles_dark.scss:
--------------------------------------------------------------------------------
1 | @import "theme_dark";
2 | @import "normalize";
3 | @import "reboot";
4 | @import "styles";


--------------------------------------------------------------------------------
/sass/_styles_light.scss:
--------------------------------------------------------------------------------
1 | @import "theme_light";
2 | @import "normalize";
3 | @import "reboot";
4 | @import "styles";


--------------------------------------------------------------------------------
/sass/_theme_dark.scss:
--------------------------------------------------------------------------------
 1 | $accent1: #2e4460;
 2 | $accent2: #de9e59;
 3 | $accent3: #c9d44c;
 4 | $accent4: #cc4523;
 5 | $accent5: #09687C;
 6 | $accent6: #378eda;
 7 | $accent7: #DFA86A;
 8 | 
 9 | $body-bg: #2B2B2B !default;
10 | $body-color: #bbbbbb !default;
11 | $section-bg: #3C3F41 !default;
12 | $section-highlight: darken($section-bg, 5%) !default;
13 | $section-content-bg: lighten($section-bg, 3%);
14 | $section-content-highlight: lighten($section-bg, 1%);
15 | $header-bg: #2e4460 !default;
16 | 
17 | $text-muted: #909090 !default;
18 | 
19 | $pagination-bg: #63676c !default;
20 | $pagination-color: #b4bcc1 !default;
21 | 
22 | $thread-sticky: #2d3d49;
23 | $thread-locked: #4b4c4e;
24 | 
25 | $link-color: $accent6 !default;
26 | $link-decoration: none !default;
27 | $link-hover-color: #44a7ff !default;
28 | $link-hover-decoration: none !default;
29 | 
30 | $scrollbar-track-bg: darken($section-bg, 5%);
31 | $scrollbar-drag-bg: #222425;
32 | $scrollbar-drag-hover-bg: $accent7;
33 | 
34 | .prettyprint {
35 |   background-color: #2b2b2b;
36 |   color: #a9b7c6;
37 | 
38 |   .lit { // literal
39 |     color: #6897bb;
40 |   }
41 | 
42 |   .kwd { // keyword
43 |     font-weight: bold;
44 |     color: #cc7832;
45 |   }
46 | 
47 |   .typ { // type
48 |     color: #4e807d;
49 |   }
50 | 
51 |   .str { // string
52 |     color: #6a8759;
53 |   }
54 | 
55 |   .pun { // punctuation
56 |     color: #e8bf6a;
57 |   }
58 | 
59 |   .com { // comment
60 |     color: #629755;
61 |     font-style: italic;
62 |   }
63 | 
64 |   .pln { // plain
65 |     color: #a9b7c6;
66 |   }
67 | 
68 |   .dec { // declaration
69 |     color: #e8bf6a;
70 |   }
71 | 
72 |   .tag { // html tag
73 |     color: #e8bf6a;
74 |   }
75 | 
76 |   .atn { // html attribute name
77 |     color: #a9b7c6;
78 |   }
79 | 
80 |   .atv { // html attribute value
81 |     color: #a5c261;
82 |   }
83 | }


--------------------------------------------------------------------------------
/sass/_theme_light.scss:
--------------------------------------------------------------------------------
 1 | $accent1: #26303d;
 2 | $accent2: #de9e59;
 3 | $accent3: #c9d44c;
 4 | $accent4: #cb3d35;
 5 | $accent5: #1b4e7e;
 6 | $accent6: #185b98;
 7 | $accent7: #DFA86A;
 8 | 
 9 | $body-bg: #EFF1F3 !default;
10 | $body-color: #373a3c !default;
11 | $section-bg: #fff !default;
12 | $section-highlight: darken($section-bg, 14%) !default;
13 | $section-content-bg: darken($section-bg, 2%);
14 | $section-content-highlight: darken($section-bg, 4%);
15 | $header-bg: #26303d;
16 | 
17 | $text-muted: #8c9195 !default;
18 | 
19 | $pagination-bg: #BCC1C5 !default;
20 | $pagination-color: #fbf9f9 !default;
21 | 
22 | $thread-sticky: lighten($accent1, 75%);
23 | $thread-locked: darken(#fff, 5%);
24 | 
25 | $link-color: $accent6 !default;
26 | $link-decoration: none !default;
27 | $link-hover-color: darken($accent6, 20%) !default;
28 | $link-hover-decoration: none !default;
29 | 
30 | $scrollbar-track-bg: darken($section-bg, 15%);
31 | $scrollbar-drag-bg: darken($section-bg, 30%);
32 | $scrollbar-drag-hover-bg: $accent7;
33 | 
34 | .prettyprint {
35 |   background-color: #ffffff;
36 |   color: #000000;
37 | 
38 |   .lit { // literal
39 |     color: #0000ff;
40 |   }
41 | 
42 |   .kwd { // keyword
43 |     font-weight: bold;
44 |     color: #000080;
45 |   }
46 | 
47 |   .typ { // type
48 |     color: #20999d;
49 |   }
50 | 
51 |   .str { // string
52 |     color: #008000;
53 |   }
54 | 
55 |   .pun { // punctuation
56 |     color: #000000;
57 |   }
58 | 
59 |   .com { // comment
60 |     color: #808080;
61 |     font-style: italic;
62 |   }
63 | 
64 |   .pln { // plain
65 |     color: #000000;
66 |   }
67 | 
68 |   .dec { // declaration
69 |     color: #000080;
70 |   }
71 | 
72 |   .tag { // html tag
73 |     color: #000080;
74 |   }
75 | 
76 |   .atn { // html attribute name
77 |     color: #0000ff;
78 |   }
79 | 
80 |   .atv { // html attribute value
81 |     color: #008000;
82 |   }
83 | }


--------------------------------------------------------------------------------
/sass/documentation_dark.scss:
--------------------------------------------------------------------------------
1 | $font-size-code: 0.9rem;
2 | @import "styles_dark";
3 | @import "sidebars";
4 | @import "scrollpane";
5 | @import "docs";
6 | @import "highlighting_dark";


--------------------------------------------------------------------------------
/sass/documentation_light.scss:
--------------------------------------------------------------------------------
1 | $font-size-code: 0.9rem;
2 | @import "styles_light";
3 | @import "sidebars";
4 | @import "scrollpane";
5 | @import "docs";
6 | @import "highlighting_light";


--------------------------------------------------------------------------------
/sass/forums_dark.scss:
--------------------------------------------------------------------------------
1 | @import "styles_dark";
2 | @import "sidebars";
3 | @import "forums";


--------------------------------------------------------------------------------
/sass/forums_light.scss:
--------------------------------------------------------------------------------
1 | @import "styles_light";
2 | @import "sidebars";
3 | @import "forums";


--------------------------------------------------------------------------------
/sass/website_dark.scss:
--------------------------------------------------------------------------------
1 | @import "styles_dark";
2 | @import "sidebars";
3 | @import "downloads";
4 | @import "scrollpane";
5 | @import "privacy";
6 | 


--------------------------------------------------------------------------------
/sass/website_light.scss:
--------------------------------------------------------------------------------
1 | @import "styles_light";
2 | @import "sidebars";
3 | @import "downloads";
4 | @import "scrollpane";
5 | @import "privacy";
6 | 


--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
 1 | pluginManagement {
 2 |     repositories {
 3 |         gradlePluginPortal()
 4 |         maven {
 5 |             name = 'MinecraftForge'
 6 |             url = 'https://maven.minecraftforge.net/'
 7 |         }
 8 |     }
 9 | }
10 | 
11 | plugins {
12 |     id 'org.gradle.toolchains.foojay-resolver-convention' version '0.7.0'
13 | }
14 | 
15 | rootProject.name = 'web'


--------------------------------------------------------------------------------
/templates/base_page.html:
--------------------------------------------------------------------------------
1 | {% include 'page_header.html' %}
2 | {% include 'page_body.html' %}
3 | {% include 'page_footer.html' %}


--------------------------------------------------------------------------------
/templates/page_directory_body.html:
--------------------------------------------------------------------------------
 1 | 
2 |
3 |

Tracked project index

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for mvn, info in promos|dictsort %} 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 | 23 |
ProjectMavenTimeLast version
{{ info.name }}{{ mvn }}{{ info.last.timestamp | todatetime | humanformatdate }}{{ info.last.version }}{% if info.last.mc and info.last.mc != 'default' %} for Minecraft {{ info.last.mc }}{% endif %}
24 |
25 |
26 | -------------------------------------------------------------------------------- /templates/page_footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Copyright © 2018-{{ now.year }} by Forge Development LLC · Ads by Longitude Ads LLC · Status · Privacy Information {%- if md.global_config['ad_footer'] %}{{ md.global_config['ad_footer'] }}{%- endif %}
4 | Layout is designed by and used with permission from PaleoCrafter 5 |
6 | Enable Dark Theme 7 | 11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | {%- if md.global_config['enable_adsense'] %} 20 | 21 | {%- else %} 22 | 23 | {%- endif %} 24 | 25 | 26 | {% if artifact and artifact.config['analytics'] %}{{ artifact.config['analytics'] }}{% endif %} 27 | 28 | {%- if artifact and artifact.config['body_end'] %} 29 | {%- for line in artifact.config['body_end'] %} 30 | {{ line }} 31 | {%- endfor %} 32 | {%- endif %} 33 | 34 | 35 | -------------------------------------------------------------------------------- /templates/page_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% if artifact %} 13 | 14 | 15 | Downloads for {{ artifact.fullname() }}{%- if mc_version %} for Minecraft {{ mc_version }}{%- endif %} 16 | 17 | {% else %} 18 | 19 | 20 | Project index 21 | 22 | {% endif %} 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 | 53 | 54 | 55 | {%- if artifact and artifact.config['body_start'] %} 56 | {%- for line in artifact.config['body_start'] %} 57 | {{ line }} 58 | {%- endfor %} 59 | {%- endif %} 60 |
61 |
62 | 66 | 75 |
76 |
77 | 78 | {%- if md.global_config['enable_adsense'] %} 79 |
80 |
81 |
82 | This site uses cookies and local storage to enable ads relevant to you and for switching to a dark theme. 83 | Unless you give us your consent, you can't use these features. Note that you can change your choice or check out more detailed information at any time. 84 |
85 | 89 |
90 |
91 | {%- endif %} 92 | -------------------------------------------------------------------------------- /templates/project_index.html: -------------------------------------------------------------------------------- 1 | {% include 'page_header.html' %} 2 | {% include 'page_directory_body.html' %} 3 | {% include 'page_footer.html' %} --------------------------------------------------------------------------------