You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ma...@apache.org on 2026/02/04 16:10:01 UTC
(grails-static-website) 02/04: Refactor and update Plugins page and some styling
This is an automated email from the ASF dual-hosted git repository.
matrei pushed a commit to branch refactor-and-plugins
in repository https://gitbox.apache.org/repos/asf/grails-static-website.git
commit 78f3608453caaba8378f76c3b579e54cfadd4985
Author: Mattias Reichel <ma...@gmail.com>
AuthorDate: Tue Feb 3 15:00:35 2026 +0100
Refactor and update Plugins page and some styling
---
assets/javascripts/navigation.js | 4 +-
assets/javascripts/paginate.js | 83 ++-
assets/javascripts/plugins-search.js | 244 +++----
assets/javascripts/plugins.js | 81 ++-
assets/javascripts/search.js | 326 +++++----
assets/stylesheets/foundation.css | 2 +-
assets/stylesheets/paginate.css | 39 +-
assets/stylesheets/plugin.css | 660 +++++++++++++++++-
assets/stylesheets/screen.css | 739 +++++++++++----------
assets/stylesheets/support-schedule.css | 2 +-
assets/stylesheets/support.css | 2 +-
buildSrc/build.gradle | 2 +-
buildSrc/settings.gradle | 0
buildSrc/src/main/groovy/org/grails/Content.groovy | 17 -
.../groovy/org/grails/ContentAndMetadata.groovy | 9 -
.../src/main/groovy/org/grails/HtmlMinutes.groovy | 7 -
.../src/main/groovy/org/grails/HtmlPost.groovy | 12 -
.../main/groovy/org/grails/MinutesMetadata.groovy | 22 -
.../org/grails/MinutesMetadataAdaptor.groovy | 38 --
.../src/main/groovy/org/grails/PageElement.groovy | 9 -
.../src/main/groovy/org/grails/PostMetadata.groovy | 22 -
.../groovy/org/grails/PostMetadataAdapter.groovy | 43 --
.../grails/documentation/DocumentationPage.groovy | 167 -----
.../org/grails/documentation/DownloadPage.groovy | 240 -------
.../org/grails/documentation/GuideGroup.groovy | 12 -
.../org/grails/documentation/GuideGroupHtml.groovy | 61 --
.../org/grails/documentation/GuideGroupItem.groovy | 12 -
.../org/grails/documentation/ProfilesPage.groovy | 104 ---
.../groovy/org/grails/documentation/SiteMap.groovy | 62 --
.../org/grails/documentation/Snapshot.groovy | 63 --
.../src/main/groovy/org/grails/events/Event.groovy | 19 -
.../groovy/org/grails/events/EventsPage.groovy | 47 --
.../groovy/org/grails/gradle/tasks/BlogTask.groovy | 701 -------------------
.../org/grails/gradle/tasks/CopyAssetsTask.groovy | 145 ----
.../org/grails/gradle/tasks/PluginsTask.groovy | 221 ------
.../main/groovy/org/grails/guides/Category.groovy | 17 -
.../org/grails/guides/GrailsVersionedGuide.groovy | 30 -
.../src/main/groovy/org/grails/guides/Guide.groovy | 15 -
.../groovy/org/grails/guides/GuidesFetcher.groovy | 121 ----
.../org/grails/guides/MultiLanguageGuide.groovy | 23 -
.../org/grails/guides/ProgrammingLanguage.groovy | 13 -
.../groovy/org/grails/guides/SingleGuide.groovy | 19 -
.../org/grails/guides/SingleLanguageGuide.groovy | 19 -
.../main/groovy/org/grails/guides/TagUtils.groovy | 61 --
.../main/groovy/org/grails/guides/Training.groovy | 19 -
.../src/main/groovy/org/grails/plugin/Owner.groovy | 9 -
.../main/groovy/org/grails/plugin/Plugin.groovy | 34 -
.../groovy/org/grails/plugin/PluginsPage.groovy | 328 ---------
.../groovy/org/grails/questions/Question.groovy | 9 -
.../org/grails/questions/QuestionHtml.groovy | 32 -
.../org/grails/questions/QuestionsPage.groovy | 36 -
.../src/main/groovy/org/grails/tags/Tag.groovy | 30 -
.../main/groovy/org/grails/tags/TagCloud.groovy | 28 -
.../gradle/GrailsWebsiteExtension.groovy | 6 +-
.../gradle/GrailsWebsitePlugin.groovy | 66 +-
.../gradle/tasks/AssetsTask.groovy} | 58 +-
.../groovy/website/gradle/tasks/BlogTask.groovy | 523 +++++++++++++++
.../gradle/tasks/DocumentationTask.groovy | 19 +-
.../gradle/tasks/DownloadTask.groovy | 18 +-
.../gradle/tasks/GrailsWebsiteTask.groovy | 6 +-
.../gradle/tasks/GuidesTask.groovy | 55 +-
.../gradle/tasks/HtaccessTask.groovy | 29 +-
.../gradle/tasks/MinutesTask.groovy | 54 +-
.../groovy/website/gradle/tasks/PluginsTask.groovy | 266 ++++++++
.../gradle/tasks/ProfilesTask.groovy | 25 +-
.../gradle/tasks/QuestionsTask.groovy | 18 +-
.../gradle/tasks/RenderSiteTask.groovy | 41 +-
.../gradle/tasks/SitemapTask.groovy | 10 +-
.../model/Content.groovy} | 17 +-
.../model/ContentAndMetadata.groovy} | 14 +-
.../model/HtmlMinutes.groovy} | 11 +-
.../model/HtmlPost.groovy} | 16 +-
.../model}/MarkdownMinutes.groovy | 2 +-
.../grails => website/model}/MarkdownPost.groovy | 4 +-
.../model/MinutesMetadata.groovy} | 24 +-
.../model/MinutesMetadataAdaptor.groovy} | 43 +-
.../{org/grails => website/model}/Page.groovy | 9 +-
.../model/PageElement.groovy} | 11 +-
.../model/PostMetadata.groovy} | 26 +-
.../model/PostMetadataAdapter.groovy} | 48 +-
.../model/documentation/DocumentationPage.groovy | 254 +++++++
.../model/documentation/DownloadPage.groovy | 286 ++++++++
.../model/documentation/GuideGroup.groovy} | 16 +-
.../model/documentation/GuideGroupHtml.groovy | 84 +++
.../model/documentation/GuideGroupItem.groovy} | 15 +-
.../model/documentation/ProfilesPage.groovy | 138 ++++
.../website/model/documentation/SiteMap.groovy | 61 ++
.../website/model/documentation/Snapshot.groovy | 56 ++
.../model}/documentation/SoftwareVersion.groovy | 24 +-
.../model/events/Event.groovy} | 26 +-
.../groovy/website/model/events/EventsPage.groovy | 69 ++
.../model/guides/Category.groovy} | 14 +-
.../model/guides/GrailsVersionedGuide.groovy} | 32 +-
.../model/guides/Guide.groovy} | 29 +-
.../website/model/guides/GuidesFetcher.groovy | 226 +++++++
.../model}/guides/GuidesPage.groovy | 145 ++--
.../model/guides/MultiLanguageGuide.groovy} | 39 +-
.../model/guides/ProgrammingLanguage.groovy} | 26 +-
.../model/guides/SingleGuide.groovy} | 34 +-
.../model/guides/SingleLanguageGuide.groovy} | 33 +-
.../groovy/website/model/guides/TagUtils.groovy | 51 ++
.../model/guides/Training.groovy} | 24 +-
.../model/plugin/Owner.groovy} | 15 +-
.../main/groovy/website/model/plugin/Plugin.groovy | 75 +++
.../model/plugin/PluginVersion.groovy} | 33 +-
.../groovy/website/model/plugin/PluginsPage.groovy | 351 ++++++++++
.../model/questions/Question.groovy} | 13 +-
.../model/questions/QuestionHtml.groovy} | 39 +-
.../website/model/questions/QuestionsPage.groovy | 54 ++
.../model/tags/Tag.groovy} | 36 +-
.../model/tags/TagCloud.groovy} | 42 +-
.../{org/grails => website}/utils/DateUtils.groovy | 53 +-
.../grails => website}/utils/MarkdownUtils.groovy | 2 +-
.../grails => website}/utils/ReadFileUtils.groovy | 10 +-
.../main/groovy/website/utils/RenderUtils.groovy | 61 ++
.../documentation/SoftwareVersionSpec.groovy | 2 +-
pages/404.html | 2 +-
.../casestudies/boosting-agility-with-grails.html | 12 +-
...y-enterprise-scale-application-development.html | 12 +-
...rehensive-solution-for-a-public-university.html | 8 +-
.../grails-increases-developer-productivity.html | 12 +-
...ghput-and-delivery-performance-with-grails.html | 20 +-
pages/casestudies/index.html | 2 +-
pages/community.html | 50 +-
pages/index.html | 16 +-
pages/learning.html | 14 +-
pages/legacy-plugins.html | 2 +-
pages/maturity.html | 2 +-
pages/search.html | 2 +-
pages/support-schedule.html | 4 +-
pages/support.html | 2 +-
pages/trademark-policy.html | 2 +-
settings.gradle | 4 +
templates/document.html | 71 +-
134 files changed, 4859 insertions(+), 4325 deletions(-)
diff --git a/assets/javascripts/navigation.js b/assets/javascripts/navigation.js
index 885e4926bbc..4770d15d7a4 100644
--- a/assets/javascripts/navigation.js
+++ b/assets/javascripts/navigation.js
@@ -1,4 +1,4 @@
function show(showId, hideId) {
- document.getElementById(showId).style.display = "block"
- document.getElementById(hideId).style.display = "none"
+ document.getElementById(showId).style.display = 'block'
+ document.getElementById(hideId).style.display = 'none'
}
\ No newline at end of file
diff --git a/assets/javascripts/paginate.js b/assets/javascripts/paginate.js
index cedeb231b4a..a7ec7b6a9ae 100644
--- a/assets/javascripts/paginate.js
+++ b/assets/javascripts/paginate.js
@@ -1,53 +1,70 @@
-const paginationContainerClass = ".pagination-container";
-const max = 20;
+const max = 20
+const paginationContainerClass = '.pagination-container'
+
function paginate(items, itemsPerPage, itemsContainer, paginationContainer) {
- let currentPage = 1;
- const totalPages = Math.ceil(items.length / itemsPerPage);
+ let currentPage = 1
+ const totalPages = Math.ceil(items.length / itemsPerPage)
function showItems(page) {
- const startIndex = (page - 1) * itemsPerPage;
- const endIndex = startIndex + itemsPerPage;
- const pageItems = items.slice(startIndex, endIndex);
+ const startIndex = (page - 1) * itemsPerPage
+ const endIndex = startIndex + itemsPerPage
+ const pageItems = items.slice(startIndex, endIndex)
- itemsContainer.innerHTML = "";
+ itemsContainer.innerHTML = ""
pageItems.forEach((item) => {
- const li = document.createElement("li");
- li.innerText = item.innerText;
- itemsContainer.appendChild(item);
- });
- window.scrollTo({top: queryInputOffsetTop, behavior: 'smooth'});
+ const li = document.createElement('li')
+ li.innerText = item.innerText
+ itemsContainer.appendChild(item)
+ })
}
- function setupPagination() {
- const pagination = document.querySelector(paginationContainer);
- pagination.innerHTML = "";
-
+ function createPaginationLinks(paginationElement) {
+ const isBottomPagination = paginationElement.classList.contains('bottom')
+ paginationElement.innerHTML = ''
if (totalPages > 1) {
for (let i = 1; i <= totalPages; i++) {
- const link = document.createElement("a");
- link.href = "#";
- link.innerText = i;
-
+ const link = document.createElement('a')
+ link.href = 'javascript:void(0)'
+ link.innerText = `${i}`
if (i === currentPage) {
- link.classList.add("active");
+ link.classList.add('active')
}
+ link.addEventListener('click', (event) => {
+ event.preventDefault()
+ currentPage = i
- link.addEventListener("click", (event) => {
- event.preventDefault();
- currentPage = i;
- showItems(currentPage);
+ // Calculate distance from bottom of page before redraw (only for bottom pagination)
+ const scrollBottomOffset = isBottomPagination
+ ? document.documentElement.scrollHeight - window.scrollY
+ : null
- const currentActive = pagination.querySelector(".active");
- currentActive.classList.remove("active");
- link.classList.add("active");
- });
+ showItems(currentPage)
- pagination.appendChild(link);
+ // Restore scroll position relative to bottom (only for bottom pagination)
+ if (isBottomPagination) {
+ window.scrollTo(0, document.documentElement.scrollHeight - scrollBottomOffset)
+ }
+
+ // Update active state on all pagination containers
+ document.querySelectorAll(paginationContainer + ' a.active').forEach(a => a.classList.remove('active'))
+ document.querySelectorAll(paginationContainer + ' a').forEach(a => {
+ if (a.innerText === `${i}`) {
+ a.classList.add('active')
+ }
+ })
+ })
+ paginationElement.appendChild(link)
}
}
}
- showItems(currentPage);
- setupPagination();
+ function setupPagination() {
+ // Populate all pagination containers (top and bottom)
+ const paginationElements = document.querySelectorAll(paginationContainer)
+ paginationElements.forEach(el => createPaginationLinks(el))
+ }
+
+ showItems(currentPage)
+ setupPagination()
}
\ No newline at end of file
diff --git a/assets/javascripts/plugins-search.js b/assets/javascripts/plugins-search.js
index e1309eb83f5..5a53e7ead44 100644
--- a/assets/javascripts/plugins-search.js
+++ b/assets/javascripts/plugins-search.js
@@ -1,31 +1,65 @@
-const queryInputFieldId = "query";
-const mobileQueryInputFieldId = "mobilequery";
-const allPluginsContainerDivClass = "allplugins";
-const pluginContainerDivClassName = "plugin";
-const allPluginsHeadingLabelClassName = "allpluginslabel";
-const searchResultsDivClassName = "searchresults";
-const searchResultsHeadingLabelClassName = "searchresultslabel";
-const searchResultsLabelSelector = "h3." + searchResultsHeadingLabelClassName;
-const gitHubStarsSelector = "div.githubstar";
-const noresultsDivClassName = "noresults";
-
-const allPlugins = [];
-const elementsClassNames = [allPluginsContainerDivClass, allPluginsHeadingLabelClassName];
-
-window.addEventListener("load", (event) => {
- const elements = document.querySelectorAll("div." + allPluginsContainerDivClass + " ul > li.plugin");
+const queryInputFieldId = 'query'
+const mobileQueryInputFieldId = 'mobile-query'
+const allPluginsContainerDivClass = 'all-plugins'
+const pluginContainerDivClassName = 'plugin'
+const allPluginsHeadingLabelClassName = 'all-plugins-label'
+const searchResultsDivClassName = 'search-results'
+const searchResultsHeadingLabelClassName = 'search-results-label'
+const searchResultsLabelSelector = 'h3.' + searchResultsHeadingLabelClassName
+const gitHubStarsSelector = 'div.github-star'
+const noresultsDivClassName = 'no-results'
+const allPlugins = []
+const elementsClassNames = [allPluginsContainerDivClass, allPluginsHeadingLabelClassName]
+
+// Tab navigation
+document.addEventListener('DOMContentLoaded', () =>{
+ const tabs = document.querySelectorAll('.plugins-nav .nav-tab')
+ const tabContents = document.querySelectorAll('.tab-content')
+ tabs.forEach(tab => {
+ tab.addEventListener('click', e => {
+ e.preventDefault()
+
+ // Remove the active class from all tabs and contents
+ tabs.forEach(t => t.classList.remove('active'))
+ tabContents.forEach(c => c.classList.remove('active'))
+
+ // Add active class to clicked tab
+ tab.classList.add('active')
+
+ // Show corresponding content
+ const tabId = tab.getAttribute('data-tab')
+ const content = document.getElementById(tabId)
+ if (content) {
+ content.classList.add('active')
+ }
+
+ // Update URL hash
+ history.replaceState(null, null, '#' + tabId)
+ })
+ })
+
+ // Handle initial hash on page load
+ const hash = window.location.hash.substring(1)
+ if (hash) {
+ const tab = document.querySelector('.nav-tab[data-tab="' + hash + '"]')
+ if (tab) {
+ tab.click()
+ }
+ }
+})
+
+window.addEventListener('load', () => {
+ const elements = document.querySelectorAll(`div.${allPluginsContainerDivClass} ul > li.plugin`)
for (let i = 0; i <= elements.length - 1; i++) {
- const element = elements[i];
- const pluginData = element.innerHTML;
- const name = element.getElementsByClassName('name');
- const desc = element.getElementsByClassName('desc')[0]?.textContent;
- const owner = element.getElementsByClassName('owner');
- const labels = element.getElementsByClassName('label');
- const vcsUrl = element.querySelector("h3.name > a").href
- const metaInfo = element.querySelector("p")?.outerHTML
- const ghStar = element.querySelector(gitHubStarsSelector)?.outerHTML
-
- const plugin = {
+ const pluginData = elements[i].innerHTML
+ const name = elements[i].getElementsByClassName('name')
+ const desc = elements[i].getElementsByClassName('desc')[0]?.textContent
+ const owner = elements[i].getElementsByClassName('owner')
+ const labels = elements[i].getElementsByClassName('label')
+ const vcsUrl = elements[i].querySelector('h3.name > a').href
+ const metaInfo = elements[i].querySelector("p")?.outerHTML
+ const ghStar = elements[i].querySelector(gitHubStarsSelector)?.outerHTML
+ allPlugins.push({
pluginData: pluginData,
name: name[0]?.textContent,
desc: desc,
@@ -34,27 +68,42 @@ window.addEventListener("load", (event) => {
vcsUrl: vcsUrl,
metaInfo: metaInfo,
ghStar: ghStar
- };
-
- allPlugins.push(plugin);
+ })
}
if (document.getElementById(queryInputFieldId)) {
- const e = document.getElementById(queryInputFieldId);
- e.oninput = onQueryChanged;
- e.onpropertychange = e.oninput; // for IE8
+ const queryInput = document.getElementById(queryInputFieldId)
+ const searchBox = queryInput.closest('.search-box-inline')
+ const clearBtn = searchBox?.querySelector('.search-clear-btn')
+
+ queryInput.addEventListener('input', onQueryChanged)
+
+ // Update the has-value class on input
+ queryInput.addEventListener('input', () => {
+ if (searchBox) {
+ searchBox.classList.toggle('has-value', queryInput.value.length > 0)
+ }
+ })
+
+ // Clear button functionality
+ if (clearBtn) {
+ clearBtn.addEventListener('click', () => {
+ queryInput.value = ''
+ searchBox.classList.remove('has-value')
+ resetDefault()
+ queryInput.focus()
+ })
+ }
}
if (document.getElementById(mobileQueryInputFieldId)) {
- const e = document.getElementById(mobileQueryInputFieldId);
- e.oninput = onQueryChanged;
- e.onpropertychange = e.oninput; // for IE8
+ document.getElementById(mobileQueryInputFieldId)
+ .addEventListener('input', onQueryChanged)
}
});
function hideElementsToDisplaySearchResults() {
for (let i = 0; i < elementsClassNames.length; i++) {
- const className = elementsClassNames[i];
- hideElementsByClassName(className);
+ hideElementsByClassName(elementsClassNames[i])
}
}
@@ -64,145 +113,102 @@ function resetDefault() {
hideElementsByClassName(searchResultsHeadingLabelClassName)
clearSearchResultsDiv()
for (let i = 0; i < elementsClassNames.length; i++) {
- const className = elementsClassNames[i];
- showElementsByClassName(className);
+ showElementsByClassName(elementsClassNames[i])
}
- paginate(defaultPluginList, max, pluginsContainer, paginationContainerClass);
+ paginate(defaultPluginList, max, pluginsContainer, paginationContainerClass)
}
function hideElementsByClassName(className) {
- const elements = document.getElementsByClassName(className);
+ const elements = document.getElementsByClassName(className)
for (let i = 0; i < elements.length; i++) {
- const element = elements[i];
- element.classList.add("hidden");
+ elements[i].classList.add('hidden')
}
}
function showElementsByClassName(className) {
- const elements = document.getElementsByClassName(className);
+ const elements = document.getElementsByClassName(className)
for (let i = 0; i < elements.length; i++) {
- const element = elements[i];
- element.classList.remove("hidden");
+ elements[i].classList.remove('hidden')
}
}
-function labelsAtPlugin(element) {
-
- const labels = [];
- for (let y = 0; y < element.length; y++) {
- labels.push(element[y].textContent)
- }
- return labels;
-}
+const labelsAtPlugin = elements =>
+ [...elements].map(el => el.textContent);
function clearSearchResultsDiv() {
- searchResultsDiv.innerHTML = ""
+ searchResultsDiv.innerHTML = ''
}
function onQueryChanged() {
- let query = queryValue()?.trim();
- const matchingPlugins = [];
- if (query === null || query === "") {
- resetDefault();
- return;
+ let query = queryValue()?.trim()
+ const matchingPlugins = []
+ if (query === null || query === '') {
+ resetDefault()
+ return
} else if (query.length < 3) {
- return;
+ return
}
if (query !== '') {
for (let i = 0; i <= allPlugins.length - 1; i++) {
- const plugin = allPlugins[i];
+ const plugin = allPlugins[i]
if (doesPluginMatchesQuery(plugin, query)) {
- matchingPlugins.push(plugin);
+ matchingPlugins.push(plugin)
}
}
}
if (searchResultsDiv) {
if (matchingPlugins.length > 0) {
if (searchResultsDiv.parentNode.getElementsByClassName(searchResultsLabelSelector).length === 0) {
- const searchResultHeadingLabel = document.querySelector(searchResultsLabelSelector);
- const querySpan = searchResultHeadingLabel.querySelector("span");
+ const searchResultHeadingLabel = document.querySelector(searchResultsLabelSelector)
+ const querySpan = searchResultHeadingLabel.querySelector('span')
querySpan.innerHTML = queryValue()
showElementsByClassName(searchResultsHeadingLabelClassName)
}
- hideElementsToDisplaySearchResults();
- searchResultsDiv.innerHTML = renderPlugins(matchingPlugins);
+ hideElementsToDisplaySearchResults()
+ searchResultsDiv.innerHTML = renderPlugins(matchingPlugins)
paginate(Array.from(searchResultsDiv.getElementsByClassName(pluginContainerDivClassName)), max, searchResultsDiv, paginationContainerClass)
showElementsByClassName(searchResultsDivClassName)
hideElementsByClassName(noresultsDivClassName)
} else if (matchingPlugins.length === 0) {
- clearSearchResultsDiv();
- hideElementsToDisplaySearchResults();
+ clearSearchResultsDiv()
+ hideElementsToDisplaySearchResults()
showElementsByClassName(noresultsDivClassName)
- const pagination = document.querySelector(paginationContainerClass);
- pagination.innerHTML = "";
+ const pagination = document.querySelector(paginationContainerClass)
+ pagination.innerHTML = ''
}
}
}
function doesTagsMatchesQuery(tags, query) {
- for (let x = 0; x < tags.length; x++) {
- const tag = tags[x];
- if (tag.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
- return true;
- }
- }
- return false;
+ const q = query.toLowerCase()
+ return tags.some(tag => tag.toLowerCase().includes(q))
}
function doesTitleMatchesQuery(title, query) {
- if (title !== undefined) {
- title = title.toLowerCase();
- query = query.toLowerCase();
- if (title.includes(query)) {
- return true;
- }
- if (query.includes(" ")) {
- return query.split(" ").every(term => title.includes(term));
- }
- }
+ if (title == null) return false
+ const t = title.toLowerCase()
+ const q = query.toLowerCase()
+ return t.includes(q) || (q.includes(" ") && q.split(" ").every(term => t.includes(term)))
}
function doesOwnerMatchesQuery(owner, query) {
- if (owner !== undefined) {
- if (owner.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
- return true;
- }
- }
+ return owner != null && owner.toLowerCase().includes(query.toLowerCase())
}
function doesPluginMatchesQuery(guide, query) {
- return doesTitleMatchesQuery(guide.name, query) || doesOwnerMatchesQuery(guide.owner, query) || doesTagsMatchesQuery(guide.labels, query);
+ return doesTitleMatchesQuery(guide.name, query) || doesOwnerMatchesQuery(guide.owner, query) || doesTagsMatchesQuery(guide.labels, query)
}
function queryValue() {
- let value = document.getElementById(queryInputFieldId).value;
- value = value.trim();
- if (value === '') {
- if (document.getElementById(mobileQueryInputFieldId)) {
- value = document.getElementById(mobileQueryInputFieldId).value;
- value = value.trim();
- return value;
- }
- }
- return value;
+ const val = id => (document.getElementById(id)?.value ?? '').trim()
+ return val(queryInputFieldId) || val(mobileQueryInputFieldId)
}
function renderPlugins(plugins) {
- let html = "";
- html += " <ul>";
- for (let i = 0; i <= plugins.length - 1; i++) {
- html += " " + renderPluginAsHtmlLi(plugins[i]);
- }
- html += " </ul>";
-
- return html;
+ return ` <ul>${plugins.map(p => ` ${renderPluginAsHtmlLi(p)}`).join('')} </ul>`
}
function renderPluginAsHtmlLi(plugin) {
- let html = "<li class='plugin'>";
- html += plugin.pluginData
- html += "</li>"
- return html;
-}
-
+ return `<li class="plugin">${plugin.pluginData}</li>`
+}
\ No newline at end of file
diff --git a/assets/javascripts/plugins.js b/assets/javascripts/plugins.js
index 1dad11b49c0..3a28ab580f1 100644
--- a/assets/javascripts/plugins.js
+++ b/assets/javascripts/plugins.js
@@ -1,18 +1,73 @@
-let pluginsDiv;
-let defaultPluginList;
-let pluginsContainer;
-let queryInputOffsetTop = document.getElementById("query")
+let pluginsDiv
+let defaultPluginList
+let pluginsContainer
+let queryInputOffsetTop = document.getElementById('query')
let searchResultsDiv
-window.addEventListener("load", (event) => {
- searchResultsDiv = document.querySelector("div.searchresults")
- pluginsDiv = document.getElementsByClassName("allplugins");
- pluginsContainer = document.querySelector("div.plugins");
- if (queryInputOffsetTop !== null){
- queryInputOffsetTop = queryInputOffsetTop.offsetTop;
+window.addEventListener('load', () => {
+ searchResultsDiv = document.querySelector('div.search-results')
+ pluginsDiv = document.getElementsByClassName('all-plugins')
+ // Target the ul.plugin-list inside the plugins div, not the div itself
+ pluginsContainer = document.querySelector('div.plugins ul.plugin-list')
+ if (queryInputOffsetTop !== null) {
+ queryInputOffsetTop = queryInputOffsetTop.offsetTop
}
- if (pluginsDiv.length > 0 && pluginsDiv[0].display !== "none") {
- defaultPluginList = Array.from(pluginsDiv[0].getElementsByClassName("plugin")).slice();
- paginate(defaultPluginList, max, pluginsContainer, paginationContainerClass);
+ if (pluginsDiv.length > 0 && pluginsDiv[0].display !== 'none') {
+ defaultPluginList = Array.from(pluginsDiv[0].getElementsByClassName('plugin')).slice()
+ paginate(defaultPluginList, max, pluginsContainer, paginationContainerClass)
}
})
+
+// Copy Maven coordinates to clipboard
+document.addEventListener('click', async (e) => {
+ const btn = e.target.closest('.copy-btn')
+ if (!btn) return
+
+ const coords = btn.dataset.coords
+ if (!coords) return
+
+ const flash = () => {
+ const original = btn.textContent
+ btn.textContent = '✓'
+ btn.classList.add('copied')
+ setTimeout(() => {
+ btn.textContent = original
+ btn.classList.remove('copied')
+ }, 1500)
+ }
+
+ const fallbackCopy = (text) => {
+ const ta = document.createElement('textarea')
+ ta.value = text
+ document.body.appendChild(ta)
+ ta.select()
+ document.execCommand('copy')
+ ta.remove()
+ }
+
+ try {
+ await navigator.clipboard.writeText(coords)
+ flash()
+ } catch (err) {
+ console.error('Failed to copy:', err)
+ fallbackCopy(coords)
+ flash()
+ }
+})
+
+// Version dropdown click-to-toggle
+document.addEventListener('click', (e) => {
+ const dropdown = e.target.closest('.version-current')?.closest('.version-dropdown')
+ // Click outside any .version-current -> close all
+ if (!dropdown) {
+ document.querySelectorAll('.version-dropdown.open')
+ .forEach(d => d.classList.remove('open'))
+ return
+ }
+ e.stopPropagation()
+ // Close other open dropdowns
+ document.querySelectorAll('.version-dropdown.open').forEach(d => {
+ if (d !== dropdown) d.classList.remove('open')
+ })
+ dropdown.classList.toggle('open')
+})
diff --git a/assets/javascripts/search.js b/assets/javascripts/search.js
index 39dcabd29df..dae8d1d0f71 100644
--- a/assets/javascripts/search.js
+++ b/assets/javascripts/search.js
@@ -1,239 +1,227 @@
-var queryInputFieldId = "query";
-var mobileQueryInputFieldId = "mobilequery";
-var allguides = new Array();
-var guideClassName = "guide";
-var multiguideClassName = "multiguide";
-
-
-var elementsClassNames = new Array();
-elementsClassNames.push('training');
-elementsClassNames.push('latestguides');
-elementsClassNames.push('guidegroup');
-elementsClassNames.push('tagsbytopic');
-elementsClassNames.push('guidesuggestion');
-
-onload = function () {
- var elements = document.getElementsByClassName(guideClassName);
- for ( var i = 0; i < elements.length; i++ ) {
- var element = elements[i];
- var guide = { href: element.getAttribute('href'), title: element.text, tags: tagsAtGuide(element.parentNode) }; /* */
- allguides.push(guide);
- }
- elements = document.getElementsByClassName(multiguideClassName);
- for ( var i = 0; i < elements.length; i++ ) {
- var element = elements[i];
- var guide = { title: titleAtMultiguide(element), versions: versionsAtMultiguide(element) }; /* */
- allguides.push(guide);
- }
-
- if ( document.getElementById(queryInputFieldId) ) {
- var e = document.getElementById(queryInputFieldId);
- e.oninput = onQueryChanged;
- e.onpropertychange = e.oninput; // for IE8
- }
- if ( document.getElementById(mobileQueryInputFieldId) ) {
- var e = document.getElementById(mobileQueryInputFieldId);
- e.oninput = onQueryChanged;
- e.onpropertychange = e.oninput; // for IE8
- }
-};
+const allGuides = []
+const guideClassName = 'guide'
+const mobileQueryInputFieldId = 'mobile-query'
+const multiGuideClassName = 'multi-guide'
+const queryInputFieldId = 'query'
+
+
+const elementsClassNames = [
+ 'training',
+ 'latest-guides',
+ 'guide-group',
+ 'tags-by-topic',
+ 'guides-suggestion',
+]
+
+window.addEventListener('load', () => {
+ let elements = document.getElementsByClassName(guideClassName)
+ for (let i = 0; i < elements.length; i++) {
+ allGuides.push({
+ href: elements[i].getAttribute('href'),
+ title: elements[i].text,
+ tags: tagsAtGuide(elements[i].parentNode)
+ })
+ }
+ elements = document.getElementsByClassName(multiGuideClassName)
+ for (let i = 0; i < elements.length; i++) {
+ allGuides.push({
+ title: titleAtMultiGuide(elements[i]),
+ versions: versionsAtMultiGuide(elements[i])
+ })
+ }
+ if (document.getElementById(queryInputFieldId)) {
+ document.getElementById(queryInputFieldId)
+ .addEventListener('input', onQueryChanged)
+ }
+ if (document.getElementById(mobileQueryInputFieldId)) {
+ document.getElementById(mobileQueryInputFieldId)
+ .addEventListener('input', onQueryChanged)
+ }
+})
function hideElementsToDisplaySearchResults() {
- for ( var i = 0; i < elementsClassNames.length; i++) {
- var className = elementsClassNames[i];
- hideElementsByClassName(className);
+ for (let i = 0; i < elementsClassNames.length; i++) {
+ hideElementsByClassName(elementsClassNames[i])
}
}
function showElementsToDisplaySearchResults() {
- for (var i = 0; i < elementsClassNames.length; i++) {
- var className = elementsClassNames[i];
- showElementsByClassName(className);
+ for (let i = 0; i < elementsClassNames.length; i++) {
+ showElementsByClassName(elementsClassNames[i])
}
}
function hideElementsByClassName(className) {
- var elements = document.getElementsByClassName(className);
- for ( var i = 0; i < elements.length; i++ ) {
- var element = elements[i];
- element.style.display = "none";
+ const elements = document.getElementsByClassName(className)
+ for (let i = 0; i < elements.length; i++) {
+ elements[i].style.display = 'none'
}
}
function showElementsByClassName(className) {
- var elements = document.getElementsByClassName(className);
- for ( var i = 0; i < elements.length; i++ ) {
- var element = elements[i];
- element.style.display = "block";
+ const elements = document.getElementsByClassName(className)
+ for (let i = 0; i < elements.length; i++) {
+ elements[i].style.display = 'block'
}
}
-function titleAtMultiguide(element) {
- for (var y = 0; y < element.childNodes.length; y++) {
- if (element.childNodes[y].className == "title") {
- return element.childNodes[y].textContent;
+function titleAtMultiGuide(element) {
+ for (let y = 0; y < element.childNodes.length; y++) {
+ if (element.childNodes[y].className === 'title') {
+ return element.childNodes[y].textContent
}
}
- return '';
+ return ''
}
-function versionsAtMultiguide(element) {
- var versions = [];
-
- for (var y = 0; y < element.childNodes.length; y++) {
- if (element.childNodes[y].className === "align-left") {
- var versionDiv = element.childNodes[y];
- var verEl;
- var hrefEl;
- var tagsArr = [];
- for (var x = 0; x < versionDiv.childNodes.length; x++) {
- if (versionDiv.childNodes[x].className === "grailsVersion") {
+function versionsAtMultiGuide(element) {
+ const versions = []
+ for (let y = 0; y < element.childNodes.length; y++) {
+ if (element.childNodes[y].className === 'align-left') {
+ const versionDiv = element.childNodes[y]
+ let verEl
+ let hrefEl
+ const tagsArr = []
+ for (let x = 0; x < versionDiv.childNodes.length; x++) {
+ if (versionDiv.childNodes[x].className === 'grails-version') {
verEl = versionDiv.childNodes[x].textContent
- hrefEl = versionDiv.childNodes[x].getAttribute('href');
+ hrefEl = versionDiv.childNodes[x].getAttribute('href')
}
- if (versionDiv.childNodes[x].className === "tag") {
- var tag = versionDiv.childNodes[x];
- tagsArr.push(tag.textContent);
+ if (versionDiv.childNodes[x].className === 'tag') {
+ tagsArr.push(versionDiv.childNodes[x].textContent);
}
}
- versions.push({grailsVersion: verEl, href: hrefEl, tags: tagsArr})
+ versions.push({
+ grailsVersion: verEl,
+ href: hrefEl,
+ tags: tagsArr
+ })
}
}
return versions
}
function tagsAtGuide(element) {
- var tags = new Array();
-
- for (var y = 0; y < element.childNodes.length; y++) {
- if (element.childNodes[y].className == "tag") {
- var tag = element.childNodes[y];
- tags.push(tag.textContent);
+ const tags = []
+ for (let y = 0; y < element.childNodes.length; y++) {
+ if (element.childNodes[y].className === 'tag') {
+ tags.push(element.childNodes[y].textContent);
}
}
- return tags;
+ return tags
}
function onQueryChanged() {
- var query = queryValue();
- const resultsDiv = document.getElementsByClassName("searchresults")
- query = query.trim();
- if ( query === '' ) {
- showElementsToDisplaySearchResults();
- resultsDiv[0].innerHTML = "";
- return;
- }
-
- var matchingGuides = [];
- if ( query !== '' ) {
- for (var i = 0; i < allguides.length; i++) {
- var guide = allguides[i];
- if ( doesGuideMatchesQuery(guide, query) ) {
- matchingGuides.push(guide);
- }
+ const query = queryValue().trim()
+ const resultsDiv = document.getElementsByClassName('search-results')
+ if (query === '') {
+ showElementsToDisplaySearchResults()
+ resultsDiv[0].innerHTML = ''
+ return
+ }
+
+ const matchingGuides = []
+ for (let i = 0; i < allguides.length; i++) {
+ let guide = allguides[i]
+ if (doesGuideMatchQuery(guide, query) ) {
+ matchingGuides.push(guide)
}
}
- if ( matchingGuides.length > 0 ) {
- hideElementsToDisplaySearchResults();
- var html = renderGuideGroup(matchingGuides, query);
- resultsDiv[0].innerHTML = html;
-
+ if (matchingGuides.length > 0) {
+ hideElementsToDisplaySearchResults()
+ resultsDiv[0].innerHTML = renderGuideGroup(matchingGuides, query)
} else {
- resultsDiv[0].innerHTML = "<div class='guidegroup'><div class='guidegroupheader'><h2>No results found</h2></div></div>";
+ resultsDiv[0].innerHTML =
+ "<div class='guide-group'><div class='guide-group-header'><h2>No results found</h2></div></div>"
}
}
-function doesTagsMatchesQuery(tags, query) {
- for ( var x = 0; x < tags.length; x++) {
- var tag = tags[x];
- if (tag.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
- return true;
+function doesTagsMatchQuery(tags, query) {
+ for (let x = 0; x < tags.length; x++) {
+ if (tags[x].toLowerCase().indexOf(query.toLowerCase()) !== -1) {
+ return true
}
}
- return false;
+ return false
}
-function doesTitleMatchesQuery(title, query) {
+function doesTitleMatchQuery(title, query) {
if (title.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
- return true;
+ return true
}
}
-function doesGuideMatchesQuery(guide, query) {
- if (doesTitleMatchesQuery(guide.title, query)) {
- return true;
+function doesGuideMatchQuery(guide, query) {
+ if (doesTitleMatchQuery(guide.title, query)) {
+ return true
}
if (guide.tags === undefined || guide.tags === null) {
- for ( var i = 0; i < guide.versions.length; i++) {
- var version = guide.versions[i];
- if (doesTagsMatchesQuery(version.tags,query)) {
- return true;
+ for (let i = 0; i < guide.versions.length; i++) {
+ const version = guide.versions[i]
+ if (doesTagsMatchQuery(version.tags, query)) {
+ return true
}
}
} else {
- if (doesTagsMatchesQuery(guide.tags, query)) {
- return true;
+ if (doesTagsMatchQuery(guide.tags, query)) {
+ return true
}
}
-
- return false;
+ return false
}
function queryValue() {
- var value = document.getElementById(queryInputFieldId).value;
- value = value.trim();
- if ( value === '' ) {
+ const value = document.getElementById(queryInputFieldId).value.trim()
+ if (value === '') {
if (document.getElementById(mobileQueryInputFieldId)) {
- value = document.getElementById(mobileQueryInputFieldId).value;
- value = value.trim();
- return value;
+ return document.getElementById(mobileQueryInputFieldId).value.trim()
}
}
- return value;
+ return value
}
function renderGuideGroup(guides, query) {
- var html = "";
- html += "<div class='guidegroup'>";
- html += " <div class='guidegroupheader'>";
- html += " <h2>Guides Filtered by: " + queryValue() + "</h2>";
- html += " </div>";
- html += " <ul>";
- for ( var i = 0; i < guides.length; i++ ) {
- html += " " + renderGuideAsHtmlLi(guides[i], query);
- }
- html += " </ul>";
- html += "</div>";
- return html;
+ const items = guides.map(g => renderGuideAsHtmlLi(g, query)).join('');
+ return `
+ <div class="guide-group">
+ <div class="guide-group-header">
+ <h2>Guides Filtered by: ${queryValue()}</h2>
+ </div>
+ <ul>
+ ${items}
+ </ul>
+ </div>
+ `;
}
function renderGuideAsHtmlLi(guide, query) {
- var html = "<li>";
- if (guide.tags === undefined || guide.tags === null) {
- html += "<div class=\"multiguide\">";
- html += "<span class=\"title\">" + guide.title + "</span>";
- var titleMatched = doesTitleMatchesQuery(guide.title, query);
- for (var i = 0; i < guide.versions.length; i++) {
- var version = guide.versions[i];
- var tagsMatched = doesTagsMatchesQuery(version.tags, query);
- if (titleMatched || tagsMatched) {
- html += "<div class=\"align-left\">";
- html += "<a class=\"grailsVersion\" href=\""+version.href+"\">"+version.grailsVersion+"</a>"
- for (var x = 0; x < version.tags.length; x++) {
- var tag = version.tags[x];
- html += "<span style='display: none' class='tag'>" + tag + "</span>";
- }
- html += "</div>";
- }
- }
- html += "</div>";
- } else {
- html += "<a class='" + guideClassName + "' href='" + guide.href + "'>" + guide.title + "</a>";
- for (var i = 0; i < guide.tags.length; i++) {
- var tag = guide.tags[i];
- html += "<span style='display: none' class='tag'>" + tag + "</span>";
- }
- }
- html += "</li>"
- return html;
+ const hiddenTags = (tags = []) =>
+ tags.map(tag => `<span style="display: none" class="tag">${tag}</span>`).join('')
+
+ // Multi-guide (no guide.tags)
+ if (guide.tags == null) {
+ const titleMatched = doesTitleMatchQuery(guide.title, query)
+ const versionsHtml = guide.versions
+ .filter(v => titleMatched || doesTagsMatchQuery(v.tags, query))
+ .map(v => `
+ <div class="align-left">
+ <a class="grails-version" href="${v.href}">${v.grailsVersion}</a>
+ ${hiddenTags(v.tags)}
+ </div>`
+ )
+ .join('')
+
+ return `<li>
+ <div class="multi-guide">
+ <span class="title">${guide.title}</span>
+ ${versionsHtml}
+ </div>
+ </li>`;
+ }
+
+ // Single guide (has guide.tags)
+ return `<li>
+ <a class="${guideClassName}" href="${guide.href}">${guide.title}</a>
+ ${hiddenTags(guide.tags)}
+ </li>`;
}
diff --git a/assets/stylesheets/foundation.css b/assets/stylesheets/foundation.css
index 42ca115742f..1012d917008 100644
--- a/assets/stylesheets/foundation.css
+++ b/assets/stylesheets/foundation.css
@@ -553,6 +553,6 @@ tbody tr td:first-child {
min-width: 130px;
}
-body.foundation h3.columnheader {
+body.foundation h3.column-header {
margin-bottom: 0
}
diff --git a/assets/stylesheets/paginate.css b/assets/stylesheets/paginate.css
index f5bc5dfff66..73ae0dea34a 100644
--- a/assets/stylesheets/paginate.css
+++ b/assets/stylesheets/paginate.css
@@ -1,51 +1,40 @@
.pagination-container {
- display: flex;
+ display: inline-flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
- margin-top: 20px;
-}
-
-.pagination-container a {
- color: black;
- float: left;
- padding: 8px 16px;
- text-decoration: none;
-}
-
-.pagination-container {
- display: inline-flex;
- border-radius: 5px;
+ border-radius: 4px;
overflow: hidden;
}
.pagination-container a {
- color: black;
- padding: 8px 16px;
+ color: #555;
+ padding: 4px 10px;
text-decoration: none;
border: 1px solid #ddd;
border-right: none;
- background-color: #f1f1f1;
- transition: background-color 0.3s ease-in-out;
+ background-color: #f5f5f5;
+ font-size: 0.85em;
+ transition: background-color 0.2s ease-in-out;
}
.pagination-container a:first-child {
- border-top-left-radius: 5px;
- border-bottom-left-radius: 5px;
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
}
.pagination-container a:last-child {
border-right: 1px solid #ddd;
- border-top-right-radius: 5px;
- border-bottom-right-radius: 5px;
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
}
.pagination-container a.active {
- background-color: #4caf50;
+ background-color: #feb672;
color: white;
- border: none;
+ border-color: #feb672;
}
.pagination-container a:hover:not(.active) {
- background-color: #ddd;
+ background-color: #e8e8e8;
}
diff --git a/assets/stylesheets/plugin.css b/assets/stylesheets/plugin.css
index da7a8318a87..e95902e8ab2 100644
--- a/assets/stylesheets/plugin.css
+++ b/assets/stylesheets/plugin.css
@@ -1,25 +1,667 @@
-ul.plugin, ol.plugin {
+/* Plugin page layout */
+.plugins-page {
+ max-width: 100%;
+ margin: 0 auto;
+ padding: 20px;
+ box-sizing: border-box;
+}
+
+/* Override screen.css .content styles for plugins page */
+.content.plugins-page {
+ max-width: 1141px !important;
+ width: 95% !important;
+ overflow: hidden !important;
+}
+
+/* Tab navigation */
+.plugins-nav {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-bottom: 20px;
+ border-bottom: 2px solid #eee;
+ padding-bottom: 0;
+}
+
+.plugins-nav .nav-tab {
+ padding: 10px 20px;
+ text-decoration: none;
+ color: #666;
+ font-weight: 500;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -2px;
+ transition: color 0.2s, border-color 0.2s;
+}
+
+.plugins-nav .nav-tab:hover {
+ color: #333;
+}
+
+.plugins-nav .nav-tab.active {
+ color: #feb672;
+ border-bottom-color: #feb672;
+}
+
+/* Tab content */
+.tab-content {
+ display: none;
+ width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+}
+
+.tab-content.active {
+ display: block;
+}
+
+/* Search and pagination row */
+.search-pagination-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ gap: 15px;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.search-pagination-row.bottom-only {
+ margin-top: 20px;
+ margin-bottom: 0;
+ justify-content: center;
+}
+
+.search-box-inline {
+ flex: 0 1 300px;
+ min-width: 150px;
+ position: relative;
+}
+
+.search-box-inline input {
+ width: 100%;
+ padding: 8px 30px 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 0.85em;
+ box-sizing: border-box;
+}
+
+.search-box-inline input:focus {
+ outline: none;
+ border-color: #feb672;
+}
+
+.search-clear-btn {
+ position: absolute;
+ right: 6px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ color: #999;
+ font-size: 1.2em;
+ cursor: pointer;
+ padding: 0 4px;
+ line-height: 1;
+ display: none;
+}
+
+.search-clear-btn:hover {
+ color: #666;
+}
+
+.search-box-inline.has-value .search-clear-btn {
+ display: block;
+}
+
+.search-pagination-row .pagination-container {
+ flex-shrink: 0;
+}
+
+/* Plugins container */
+.plugins.allplugins {
+ display: block;
+ width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+ clear: both;
+}
+
+/* Section titles */
+.section-title {
+ font-size: 1.3em;
+ color: #333;
+ margin-bottom: 20px;
+ padding-bottom: 10px;
+ border-bottom: 2px solid #feb672;
+}
+
+/* Links section */
+.links-section {
+ max-width: 600px;
+}
+
+.links-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.links-list li {
+ margin-bottom: 20px;
+ padding: 15px;
+ background: #f8f8f8;
+ border-radius: 8px;
+}
+
+.links-list li a {
+ font-size: 1.1em;
+ font-weight: 500;
+ color: #333;
+ text-decoration: none;
+}
+
+.links-list li a:hover {
+ color: #feb672;
+}
+
+.links-list li p {
+ margin: 8px 0 0;
+ color: #666;
+ font-size: 0.9em;
+}
+
+/* Plugin list container - responsive grid */
+.plugin-list {
padding-left: 0;
+ margin: 0;
+ list-style: none;
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+ width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+}
+
+/* 1 column for small screens */
+@media screen and (max-width: 600px) {
+ .plugin-list {
+ grid-template-columns: 1fr;
+ }
}
+/* Plugin card */
li.plugin {
- padding: 24px;
- border-radius: 1px;
- box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
- -webkit-box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ background: #fff;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ position: relative;
+}
+
+/* Deprecated plugin card */
+li.plugin.deprecated {
+ opacity: 0.7;
+ background: #fafafa;
+}
+
+li.plugin.deprecated:hover {
+ opacity: 0.9;
+}
+
+.deprecated-badge {
+ display: inline-block;
+ background: #e74c3c;
+ color: white;
+ font-size: 0.65em;
+ font-weight: 600;
+ padding: 3px 8px;
+ border-radius: 3px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ cursor: help;
+ flex-shrink: 0;
+}
+
+/* Plugin header row */
+li.plugin .plugin-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 10px;
+}
+
+/* Plugin name */
+li.plugin h3.name {
+ margin: 0;
+ font-size: 1.2em;
+ line-height: 1.3;
+ flex: 1;
+ min-width: 0;
+ word-wrap: break-word;
+}
+
+li.plugin h3.name a {
+ color: #333;
+ text-decoration: none;
+}
+
+li.plugin h3.name a:hover {
+ color: #feb672;
+}
+
+/* Description */
+li.plugin p.desc {
+ margin: 0;
+ font-size: 0.9em;
+ color: #666;
+ line-height: 1.5;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+/* Version dropdown */
+.version-dropdown {
+ position: relative;
+ flex-shrink: 0;
+}
+
+.version-current {
+ display: flex;
+ flex-direction: column;
+ border-radius: 4px;
+ overflow: hidden;
+ font-size: 0.75em;
+ border: 1px solid #ddd;
+ cursor: pointer;
+ user-select: none;
+}
+
+.version-current:hover {
+ border-color: #ccc;
+}
+
+.version-current .version-row {
+ display: flex;
+ align-items: stretch;
+ white-space: nowrap;
+}
+
+.version-current .version {
+ background: #f0f0f0;
+ color: #555;
+ padding: 2px 8px;
+ font-weight: 600;
+ flex-grow: 1;
+}
+
+/* When version is the only element (no grails-compat), it should fill the row */
+.version-current .version:last-child {
+ border-radius: 0;
+}
+
+.version-current .grails-compat {
+ background: #f8f8f8;
+ color: #777;
+ padding: 2px 8px;
+ border-left: 1px solid #ddd;
}
-div.twocolumns div.column {
+.version-current .dropdown-arrow {
+ background: #f0f0f0;
+ color: #555;
+ padding: 2px 8px;
+ border-left: 1px solid #ddd;
+ font-size: 1em;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+}
+
+.version-current .version-date-row {
+ background: #fafafa;
+ color: #888;
+ padding: 2px 8px;
+ border-top: 1px solid #eee;
+ text-align: center;
+ font-size: 0.9em;
+}
+
+.version-dropdown.open .version-current .dropdown-arrow {
+ color: #333;
+}
+
+.version-list {
+ display: none;
+ position: absolute;
+ top: 100%;
+ right: 0;
+ margin-top: 4px;
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ z-index: 100;
+ min-width: 280px;
+ max-height: 250px;
+ overflow-y: auto;
+}
+
+.version-dropdown.open .version-list {
+ display: block;
+}
+
+.version-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 12px;
+ padding: 8px 12px;
+ font-size: 0.85em;
+ border-bottom: 1px solid #f0f0f0;
+ white-space: nowrap;
+}
+
+.version-item:last-child {
+ border-bottom: none;
+}
+
+.version-item:hover {
+ background: #f8f8f8;
+}
+
+.version-item .ver {
+ font-weight: 600;
+ color: #555;
+ flex-shrink: 0;
+}
+
+.version-item .date {
+ color: #999;
+ font-size: 0.9em;
+ flex-shrink: 0;
+}
+
+.version-item .compat {
+ color: #777;
+ font-size: 0.9em;
+ text-align: right;
+ flex-shrink: 0;
+}
+
+/* Legacy pill style (fallback) */
+.plugin-meta {
+ display: flex;
+ flex-direction: column;
+ border-radius: 4px;
+ overflow: hidden;
+ font-size: 0.75em;
+ border: 1px solid #ddd;
+ flex-shrink: 0;
+}
+
+.plugin-meta .version-row {
+ display: flex;
+ align-items: stretch;
+ white-space: nowrap;
+}
+
+.plugin-meta .version {
+ background: #f0f0f0;
+ color: #555;
+ padding: 2px 8px;
+ font-weight: 600;
+ flex-grow: 1;
+}
+
+.plugin-meta .grails-compat {
+ background: #f8f8f8;
+ color: #777;
+ padding: 2px 8px;
+ border-left: 1px solid #ddd;
+}
+
+.plugin-meta .version-date-row {
+ background: #fafafa;
+ color: #888;
+ padding: 2px 8px;
+ border-top: 1px solid #eee;
+ text-align: center;
+ font-size: 0.9em;
+}
+
+/* Maven coordinates wrapper */
+.coords-wrapper {
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+ border-radius: 4px;
+ overflow: hidden;
+ border: 1px solid #e0e0e0;
+ background: #f5f5f5;
+}
+
+.coords-wrapper a.maven-link {
+ flex: 1;
+ min-width: 0;
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+}
+
+.coords-wrapper a.maven-link:hover code.coords {
+ background: #eaeaea;
+}
+
+/* Override screen.css code { margin: 0 !important; padding: 0 !important; } */
+li.plugin .coords-wrapper code.coords {
+ display: block;
+ width: 100%;
+ background: #f5f5f5;
+ padding: 12px 14px !important;
+ margin: 0 !important;
+ font-size: 0.8em;
+ color: #555;
+ border: none;
+ border-radius: 0;
+ transition: background 0.2s;
+ line-height: 1.5;
+ word-break: break-word;
+ overflow-wrap: break-word;
+}
+
+.coords-wrapper .copy-btn {
+ flex-shrink: 0;
+ width: 44px;
+ padding: 0 14px;
+ background: #e8e8e8;
+ border: none;
+ border-left: 1px solid #ddd;
+ cursor: pointer;
+ font-size: 1em;
+ transition: background 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.coords-wrapper .copy-btn:hover {
+ background: #ddd;
+}
+
+.coords-wrapper .copy-btn.copied {
+ background: #4caf50;
+ color: white;
+}
+
+/* Plugin card footer */
+.plugin-card-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.plugin-card-footer .footer-left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.plugin-card-footer .owner-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 3px 10px;
+ background: #f0f4f8;
+ color: #5a7a9a;
+ border-radius: 12px;
+ font-size: 0.75em;
+ text-decoration: none;
+ transition: background 0.2s;
+}
+
+.plugin-card-footer .owner-pill .owner-icon {
+ font-size: 0.8em;
+ color: #5a7a9a;
+}
+
+.plugin-card-footer .owner-pill:hover {
+ background: #e0e8f0;
+ color: #4a6a8a;
+}
+
+.plugin-card-footer .owner-pill:hover .owner-icon {
+ color: #4a6a8a;
+}
+
+.plugin-card-footer .docs-link {
+ display: inline-flex;
+ align-items: center;
+ padding: 3px 8px;
+ background: #f0f0f0;
+ border-radius: 4px;
+ color: #555;
+ font-size: 0.75em;
+ text-decoration: none;
+ transition: background 0.2s;
+}
+
+.plugin-card-footer .docs-link:hover {
+ background: #e0e0e0;
+}
+
+.plugin-card-footer .license {
+ color: #888;
+ font-size: 0.7em;
+ padding: 2px 6px;
+ background: #f8f8f8;
+ border-radius: 3px;
+}
+
+/* GitHub stars - toned down, inline with tags */
+.github-stars {
+ display: inline-flex;
+ align-items: center;
+ color: #888;
+ font-size: 0.75em;
+}
+
+/* Tags row with GitHub stars */
+.plugin-tags-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ flex-wrap: wrap;
+ margin-top: auto;
+}
+
+/* Labels/tags */
+li.plugin ul.labels {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+}
+
+li.plugin ul.labels li.label {
+ padding: 0;
+ box-shadow: none;
+ background: none;
+}
+
+li.plugin ul.labels li.label a {
+ display: inline-block;
+ padding: 3px 8px;
+ background: #f0f4f8;
+ color: #5a7a9a;
+ border-radius: 3px;
+ font-size: 0.75em;
+ text-decoration: none;
+ transition: background 0.2s;
+}
+
+li.plugin ul.labels li.label a:hover {
+ background: #e0e8f0;
+}
+
+
+/* Two column layout adjustments */
+div.two-columns div.column {
width: 44.326906%;
padding: 15px;
}
-article.quote, .guidegroup {
+article.quote, .guide-group {
border-top: 0;
padding-top: 0px;
}
-.columnheader {
+.column-header {
margin-top: 50px;
- margin-bottom: 10px;
+ margin-bottom: 15px;
+ font-size: 1.3em;
+ color: #333;
+ border-bottom: 2px solid #feb672;
+ padding-bottom: 8px;
+}
+
+/* Responsive adjustments - matches main site breakpoint */
+@media screen and (max-width: 960px) {
+ li.plugin {
+ padding: 16px;
+ }
+
+ .plugins-nav {
+ gap: 2px;
+ }
+
+ .plugins-nav .nav-tab {
+ padding: 8px 12px;
+ font-size: 0.85em;
+ }
+
+ .search-pagination-row {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+ }
+
+ .search-box-inline {
+ flex: none;
+ width: 100%;
+ min-width: auto;
+ }
}
diff --git a/assets/stylesheets/screen.css b/assets/stylesheets/screen.css
index 6a4369c10dd..ebbbfd5e59c 100644
--- a/assets/stylesheets/screen.css
+++ b/assets/stylesheets/screen.css
@@ -75,22 +75,22 @@ body {
background-color: #F5F5F5;
margin: 0;
padding: 0;
- font-family: 'Roboto',"Open Sans","Helvetica Neue", Helvetica, Arial,sans-serif;
+ font-family: Roboto, 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 16px;
height: 100%;
}
-body.documentation .headerbar,
+body.documentation .header-bar,
.question h2,
-.columnheader a,
-.columnheader {
+.column-header a,
+.column-header {
color: #FFFFFF;
background-color: #C8C8C8;
}
-body.guides .columnheader a {
+body.guides .column-header a {
background-color: transparent;
}
-body.guides .columnheader a:hover {
+body.guides .column-header a:hover {
color: #FFFFFF;
text-decoration: underline;
}
@@ -108,19 +108,19 @@ ul, ol {
padding-left: 20px;
}
-.blogpost > ul, ol {
+.blog-post > ul, ol {
padding-left: 20px !important;
}
-.blogpost > ul {
+.blog-post > ul {
list-style: disc !important;
}
-.blogpost > ol, ul ol {
+.blog-post > ol, ul ol {
list-style: lower-roman !important;
}
-.blogpost > ol {
+.blog-post > ol {
list-style: lower-roman !important;
}
@@ -129,7 +129,7 @@ b, strong {
}
h4, h5, h6, b, strong {
-color: #000;
+ color: #000;
}
html, body {
@@ -139,11 +139,12 @@ html, body {
body.home {
background-color: #ffffff;
}
+
a {
text-decoration: underline;
}
-.columnheader a {
+.column-header a {
text-decoration: none;
}
@@ -162,7 +163,7 @@ h6 {
padding: 0;
}
-.tagcloud {
+.tag-cloud {
overflow: auto;
margin-bottom: 44px !important; /* 44px + 6px tag margin bottom */
}
@@ -268,7 +269,7 @@ li {
margin-bottom: 10px;
}
-.fullwidth,
+.full-width,
.content {
margin: 0 auto;
z-index: 2;
@@ -281,22 +282,22 @@ li {
}
-.listingblock .content {
+.listing-block .content {
width: auto;
max-width: 100%;
}
-.fullwidth {
+.full-width {
width: 99%;
}
-.fullwidth p,
+.full-width p,
.content p {
font-size: 18px;
line-height: 24px;
}
-.fullwidth article,
+.full-width article,
.content article {
clear: both;
margin: 50px 0 50px;
@@ -307,7 +308,7 @@ li {
}
div.code,
-.fullwidth article pre,
+.full-width article pre,
.content article pre {
background-color: #f5f5f5;
font-family: "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace;
@@ -326,7 +327,7 @@ code {
.quote,
.post,
-.usergroup,
+.user-group,
.question,
.event,
.book {
@@ -339,38 +340,38 @@ code {
.quote,
.post,
-.usergroup,
+.user-group,
.question,
.event,
.book,
-.transparent_post {
+.transparent-post {
clear: both;
overflow: auto;
margin: 0;
padding: 50px;
}
-.twocolumns .column:first-child .transparent_post {
+.two-columns .column:first-child .transparent-post {
padding-left: 0;
}
-.usergroup,
+.user-group,
.question,
.event,
.book {
margin-bottom: 50px;
}
-.column usergroup:last-child,
+.column user-group:last-child,
.column .question:last-child,
.column .event:last-child {
margin-bottom: 0;
}
-.usergroup ul { padding-left: 0; margin-left: 0; }
-.usergroup li { list-style-type: none; }
+.user-group ul { padding-left: 0; margin-left: 0; }
+.user-group li { list-style-type: none; }
-.usergroup a { color: #7c7c7c;}
+.user-group a { color: #7c7c7c;}
.event ul,
.event li,
@@ -422,81 +423,81 @@ code {
margin-top: 5px;
}
-.mainheader {
+.main-header {
padding-top: 17px;
padding-bottom: 0;
overflow: auto;
background-color: #ffffff !important;
}
-body.guide .colist th,
-body.guide .colist td {
+body.guide .co-list th,
+body.guide .co-list td {
border: none;
}
-body.guide .mainheader,
-body.grails3plugins .mainheader,
-body.gorm .mainheader {
+body.guide .main-header,
+body.grails3-plugins .main-header,
+body.gorm .main-header {
background-color: #f5f5f5 !important;
}
-#showNavigationLink {
+#show-navigation-link {
margin-top: 12px;
margin-bottom: 20px;
display: block;
}
-.secondarymenu ul {
+.secondary-menu ul {
margin: 0;
padding: 0;
}
-.mainmenu {
+.main-menu {
clear: right;
}
-#topmenus {
+#top-menus {
margin-top: 17px;
display: none;
background-color: gray;
overflow: auto;
padding: 10px;
}
-#topmenus a {
+#top-menus a {
color: #ffffff;
}
-.mainmenu,
-.secondarymenu,
-.mainmenu ul,
-.secondarymenu ul {
+.main-menu,
+.secondary-menu,
+.main-menu ul,
+.secondary-menu ul {
margin: 0;
padding: 0;
}
-.secondarymenu,
-.mainmenu {
+.secondary-menu,
+.main-menu {
float: right;
width: 50%;
clear: none;
}
@media screen and (min-width: 961px) {
- .secondarymenu,
- .mainmenu {
+ .secondary-menu,
+ .main-menu {
float: none;
width: auto;
clear: both;
}
}
-.mainmenu li,
-.secondarymenu li,
-.secondarymenu li a,
-.mainmenu li a {
+.main-menu li,
+.secondary-menu li,
+.secondary-menu li a,
+.main-menu li a {
text-align: left;
}
-.mainmenu li,
-.secondarymenu li {
+.main-menu li,
+.secondary-menu li {
margin: 0;
padding: 0;
font-size: 16px;
@@ -504,51 +505,51 @@ body.gorm .mainheader {
float: left;
}
-.secondarymenu li,
-.mainmenu li {
+.secondary-menu li,
+.main-menu li {
width: 100%;
}
-.secondarymenu li,
-.mainmenu li {
+.secondary-menu li,
+.main-menu li {
list-style-type: none;
padding: 0;
}
-.secondarymenu li a,
-.mainmenu li a {
+.secondary-menu li a,
+.main-menu li a {
display: block;
padding: 20px 5px;
text-decoration: none;
letter-spacing: 0.5px;
color: #7c7c7c;
}
-.secondarymenu li.active a,
-.mainmenu li.active a {
+.secondary-menu li.active a,
+.main-menu li.active a {
text-decoration: underline;
}
-.secondarymenu li a {
+.secondary-menu li a {
text-transform: uppercase;
}
-.headerbar {
+.header-bar {
clear: both;
text-transform: uppercase;
padding-bottom: 42.5px;
}
-.headerbar a {
+.header-bar a {
color: #ffffff;
}
-.headerbar h1 {
+.header-bar h1 {
color: #ffffff;
float: left;
font-size: 24px;
font-family: 'Roboto-Medium', "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
}
-.headerbar h1 {
+.header-bar h1 {
margin-top: 42.5px;
}
@@ -557,22 +558,22 @@ body.gorm .mainheader {
}
.training th,
-.calltoaction h2 {
+.call-to-action h2 {
background-color: gray;/*#feb672;*/
}
-.get-started-row .calltoaction h2 {
+.get-started-row .call-to-action h2 {
background-color: #255aa8;
}
-.headerbar,
+.header-bar,
#features {
background-color: #255AA8;
}
a,
a:hover,
-.partnersnav li a {
+.partners-nav li a {
color: #255AA8;
}
@@ -596,24 +597,24 @@ a:hover,
text-transform: uppercase;
font-family: 'Archia-Light', 'Roboto-Light', "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
}
-.chailicelogo {
+.chalice-logo {
display: none;
}
-.downloadlatestversion {
+.download-latest-version {
margin-top: 15px !important;
}
-.olderversions {
+.older-versions {
overflow: auto;
margin-bottom: 50px;
}
-.olderversions select {
+.older-versions select {
width: 100%;
font-size: 20px;
}
-.olderversions h4 {
+.older-versions h4 {
background-color: #ffffff;
font-size: 18px;
font-family: 'Roboto-Medium', sans-serif;
@@ -624,7 +625,7 @@ a:hover,
}
article.quote,
-.guidegroup {
+.guide-group {
border-top: 2px solid #feb672;
background-color: #ffffff;
padding-top: 12px;
@@ -634,57 +635,57 @@ article.quote,
margin-bottom: 50px;
}
-.twocolumns .column article.quote:first-child {
+.two-columns .column article.quote:first-child {
margin-top: 50px;
}
-.twocolumns .column .guidegroup:first-child {
+.two-columns .column .guide-group:first-child {
margin-top: 30px;
}
-.guidegroup p {
+.guide-group p {
margin: 0;
padding: 10px 0;
background-color: #F5F5F5;
}
-.guidegroupheader {
+.guide-group-header {
padding-left: 18px;
height: 118px;
overflow: hidden;
padding-top: 10px;
}
-.guidegroupheader a {
+.guide-group-header a {
color: #7c7c7c;
}
-.guidegroupheader img {
+.guide-group-header img {
float: left;
width: 93px;
height: 93px;
margin-right: 18px;
}
-.guidegroup h2 {
+.guide-group h2 {
height: 144px;
text-transform: uppercase;
font-size: 18px;
padding-top: 40px;
}
-.guidegroup ul {
+.guide-group ul {
padding: 0;
margin: 0;
background-color: #ffffff;
}
-div.guidegroupbody, div.guidegroupbody p {
+div.guide-group-body, div.guide-group-body p {
padding: 10px;
margin: 0;
background-color: #ffffff;
}
-.guidegroup li {
+.guide-group li {
overflow: auto;
list-style-type: none;
min-height: 60px;
@@ -693,60 +694,60 @@ div.guidegroupbody, div.guidegroupbody p {
padding: 38px 30px 0;
}
-.guidegroup li.legend {
+.guide-group li.legend {
padding-top: 0;
border: 0;
}
-.guidegroup li a {
+.guide-group li a {
display: block;
text-decoration: underline;
letter-spacing: 1px;
}
-.guidegroup li span.title {
+.guide-group li span.title {
display: block;
border: 1px solid #ffffff;
}
-.guidegroup li span.title,
-.guidegroup li a.guide {
+.guide-group li span.title,
+.guide-group li a.guide {
background-image: url(../images/caret.png);
background-repeat: no-repeat;
background-position: right center;
padding-right: 20px;
}
-.guidegroup li a.quickcast {
+.guide-group li a.quick-cast {
background-image: url(../images/video.png);
}
-.guidegroup li a {
- font-family: 'Roboto-Light', "Open Sans","Helvetica Neue", Helvetica, Arial, sans-serif;
+.guide-group li a {
+ font-family: Roboto-Light, 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #7c7c7c;
}
-.guidegroup li a.grailsVersion {
+.guide-group li a.grails-version {
float: left;
padding-right: 20px;
}
-.guidegroup li.legend a {
+.guide-group li.legend a {
background-image: none;
display: inline;
padding: 0;
}
-.sponsoredby {
+.sponsored-by {
margin-top: 30px;
}
-.sponsoredby h4 {
+.sponsored-by h4 {
text-transform: uppercase;
font-size: 14px;
margin: 0 0 35px 10px;
}
-.sponsoredby img {
+.sponsored-by img {
width: 100%;
}
-.sponsoredby, .search, .latestguides {
+.sponsored-by, .search, .latest-guides {
margin-bottom: 24px;
}
@@ -768,8 +769,8 @@ div.search {
display: block;
}
-.goldenratio,
-.twocolumns {
+.golden-ratio,
+.two-columns {
overflow: auto;
}
.column {
@@ -777,7 +778,7 @@ div.search {
overflow: hidden;
}
-.columnheader {
+.column-header {
min-height: 35px;
padding-top: 12px;
padding-bottom: 6px;
@@ -791,58 +792,58 @@ div.search {
margin-top: 50px;
margin-bottom: 50px;
}
-.transparent_post .columnheader,
-.question .columnheader,
-.post .columnheader {
+.transparent-post .column-header,
+.question .column-header,
+.post .column-header {
margin-top: 0;
margin-bottom: 20px;
}
-body.documentation .columnheader,
-body.guides .columnheader {
+body.documentation .column-header,
+body.guides .column-header {
background-color: #255AA8;
color: #ffffff;
}
-.latestguides ul {
+.latest-guides ul {
margin-bottom: 40px;
padding-left: 0;
}
-body.grails3plugins .latestguides ul {
+body.grails3-plugins .latest-guides ul {
padding: 0 0 0 20px;
}
-.latestguides li {
+.latest-guides li {
list-style-type: none;
margin-bottom: 30px;
}
-.latestguides li b {
+.latest-guides li b {
text-transform: uppercase;
font-weight: normal;
}
li.plugin h3,
-.latestguides li b,
-.latestguides li a,
-.latestguides li span {
+.latest-guides li b,
+.latest-guides li a,
+.latest-guides li span {
display: block;
margin-bottom: 5px;
}
-.latestguides li a,
-.latestguides li span {
+.latest-guides li a,
+.latest-guides li span {
font-size: 14px;
}
-.tagsbytopic ul {
+.tags-by-topic ul {
margin: 40px 0 0 0;
padding: 0;
overflow: auto;
}
-body.grails3plugins .tagsbytopic ul {
+body.grails3-plugins .tags-by-topic ul {
padding-left: 20px;
}
-.tagsbytopic li {
+.tags-by-topic li {
list-style-type: none;
background-color: #F0C45A;
float: left;
@@ -854,62 +855,62 @@ body.grails3plugins .tagsbytopic ul {
vertical-align: middle;
padding: 7px 10px 3px;
}
-.tagsbytopic li a {
+.tags-by-topic li a {
text-decoration: none;
color: #ffffff;
text-transform: uppercase;
}
-.tagsbytopic li.tag1{ font-size: 12px; background-color: #dac589; }
-.tagsbytopic li.tag2{ font-size: 12px; background-color: #f0c45b; }
-.tagsbytopic li.tag3{ font-size: 12px; background-color: #f0c45b;}
-.tagsbytopic li.tag4{ font-size: 12px; background-color: #f2c458;}
-.tagsbytopic li.tag5{ font-size: 12px; background-color: #f2c458;}
-.tagsbytopic li.tag6{ font-size: 16px; background-color: #f2c458;}
-.tagsbytopic li.tag7{ font-size: 16px; background-color: #feb672;}
-.tagsbytopic li.tag8{ font-size: 16px; background-color: #feb672;}
-.tagsbytopic li.tag9{ font-size: 16px; background-color: #feb672;}
-.tagsbytopic li.tag10{ font-size: 16px; background-color: #feb672;}
-.tagsbytopic li.tag11{ font-size: 20px; background-color: #feb672;}
-.tagsbytopic li.tag12{ font-size: 20px; background-color: #feb672;}
-.tagsbytopic li.tag13{ font-size: 20px; background-color: #feb672;}
-.tagsbytopic li.tag14{ font-size: 20px; background-color: #feb672;}
-.tagsbytopic li.tag15{ font-size: 20px; background-color: #feb672;}
-.tagsbytopic li.tag16{ font-size: 20px; background-color: #feb672;}
-.tagsbytopic li.tag17{ font-size: 20px; background-color: #feb672;}
-.tagsbytopic li.tag18{ font-size: 20px; background-color: #feb672;}
-.tagsbytopic li.tag19{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag20{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag21{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag22{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag23{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag24{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag25{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag26{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag27{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag28{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag29{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag30{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag31{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag32{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag33{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag34{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag35{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag36{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag37{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag38{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag39{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag40{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag41{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag42{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag43{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag44{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag45{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag46{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag47{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag48{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag49{ font-size: 24px; background-color: #feb672;}
-.tagsbytopic li.tag50{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag1{ font-size: 12px; background-color: #dac589; }
+.tags-by-topic li.tag2{ font-size: 12px; background-color: #f0c45b; }
+.tags-by-topic li.tag3{ font-size: 12px; background-color: #f0c45b;}
+.tags-by-topic li.tag4{ font-size: 12px; background-color: #f2c458;}
+.tags-by-topic li.tag5{ font-size: 12px; background-color: #f2c458;}
+.tags-by-topic li.tag6{ font-size: 16px; background-color: #f2c458;}
+.tags-by-topic li.tag7{ font-size: 16px; background-color: #feb672;}
+.tags-by-topic li.tag8{ font-size: 16px; background-color: #feb672;}
+.tags-by-topic li.tag9{ font-size: 16px; background-color: #feb672;}
+.tags-by-topic li.tag10{ font-size: 16px; background-color: #feb672;}
+.tags-by-topic li.tag11{ font-size: 20px; background-color: #feb672;}
+.tags-by-topic li.tag12{ font-size: 20px; background-color: #feb672;}
+.tags-by-topic li.tag13{ font-size: 20px; background-color: #feb672;}
+.tags-by-topic li.tag14{ font-size: 20px; background-color: #feb672;}
+.tags-by-topic li.tag15{ font-size: 20px; background-color: #feb672;}
+.tags-by-topic li.tag16{ font-size: 20px; background-color: #feb672;}
+.tags-by-topic li.tag17{ font-size: 20px; background-color: #feb672;}
+.tags-by-topic li.tag18{ font-size: 20px; background-color: #feb672;}
+.tags-by-topic li.tag19{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag20{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag21{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag22{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag23{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag24{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag25{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag26{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag27{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag28{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag29{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag30{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag31{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag32{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag33{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag34{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag35{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag36{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag37{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag38{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag39{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag40{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag41{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag42{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag43{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag44{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag45{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag46{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag47{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag48{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag49{ font-size: 24px; background-color: #feb672;}
+.tags-by-topic li.tag50{ font-size: 24px; background-color: #feb672;}
footer {
clear: both;
@@ -923,9 +924,34 @@ footer li {
line-height: 15px;
}
+footer .content {
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-start;
+ gap: 60px;
+}
+
.apache-grails {
- text-align: center !important;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+.apache-grails p {
+ font-size: 15px;
+ color: #666;
+ line-height: 1.6;
+ margin: 10px 0;
+ max-width: 480px;
+}
+
+.apache-grails img {
+ margin: 20px 0;
+ max-width: 300px;
+ height: auto;
}
+
.apache-grails span {
text-align: center;
display: block;
@@ -934,60 +960,95 @@ footer li {
line-height: 15px;
}
-.socialmedianav ul {
+.social-media-nav {
+ text-align: center;
+ margin: 30px 0;
+}
+
+.social-media-nav ul {
display: flex;
justify-content: center;
- padding-left: 1em;
+ padding-left: 0;
+ margin: 0;
flex-wrap: wrap;
+ gap: 10px;
}
-@media screen and (min-width: 961px) {
- .socialmedianav ul {
- flex-wrap: nowrap;
- }
-}
-.socialmedianav li {
- margin-left: 10px;
- margin-right: 10px;
+
+.social-media-nav li {
list-style-type: none;
width: 45px;
height: 45px;
+ margin: 0;
+}
+
+.social-media-nav li a {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ transition: transform 0.2s;
+}
+
+.social-media-nav li a:hover {
+ transform: scale(1.1);
+}
+
+.social-media-nav li img {
+ width: 45px;
+ height: 45px;
}
-.partnersnav ul {
+.partners-nav ul {
padding-left: 1em;
}
-.partnersnav li {
+.partners-nav li {
list-style-type: none;
color: #7C7C7C;
margin-bottom: 20px;
}
-.partnersnav li a {
+.partners-nav li a {
text-decoration: none;
}
-.apache-grails,
-.partnersnav,
-.socialmedianav {
+.footer-bottom {
text-align: center;
- margin-bottom: 30px;
+ padding: 20px;
+ background-color: #f5f5f5;
+ border-top: 1px solid #eee;
}
-.grailslogo {
+.footer-bottom p {
+ margin: 5px 0;
+ font-size: 14px;
+ color: #666;
+}
+
+.footer-bottom a {
+ color: #5a7a9a;
+ text-decoration: none;
+}
+
+.footer-bottom a:hover {
+ text-decoration: underline;
+}
+
+.grails-logo {
text-align: center;
margin: 0 auto;
display: block;
max-width: 8rem;
}
-body.home .grailslogo {
+body.home .grails-logo {
display: block;
}
-.homebar {
+.home-bar {
padding-top: 50px;
background-color: #f8f7f7 !important;
}
-.homebar h1 {
+.home-bar h1 {
text-align: center;
width: 52.58545136%; /* 680px / 1141px; */
margin: 0 auto;
@@ -1002,7 +1063,7 @@ article.quote h2 {
padding: 46px 40px;
}
-.calltoaction h2 {
+.call-to-action h2 {
padding: 10px;
width: 90%;
max-width: 270px;
@@ -1017,7 +1078,7 @@ article.quote h2 {
font-family: 'Archia-Light', 'Roboto-Light', "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
-.calltoaction {
+.call-to-action {
text-align: center;
overflow: auto;
padding-top: 30px;
@@ -1025,17 +1086,17 @@ article.quote h2 {
}
-.calltoaction.position-reset {
+.call-to-action.position-reset {
padding-top: 0;
padding-bottom: 0;
}
-.calltoaction h2 a {
+.call-to-action h2 a {
font-size: 16px;
}
-.calltoaction h2 a {
+.call-to-action h2 a {
color: #ffffff;
text-decoration: none;
width: 100%;
@@ -1043,17 +1104,17 @@ article.quote h2 {
font-weight: normal;
letter-spacing: 1px;
}
-.calltoaction .allversions {
+.call-to-action .all-versions {
margin-top: 15px;
display: block;
color: #7c7c7c;
}
-.calltoaction b {
+.call-to-action b {
margin-bottom: 10px;
font-size: 14px;
}
-.calltoaction h2 span.version,
-.calltoaction b {
+.call-to-action h2 span.version,
+.call-to-action b {
display: block;
}
@@ -1083,7 +1144,7 @@ article.quote h2 {
padding: 0;
}
-.threecolumns .column {
+.three-columns .column {
margin-top: 20px;
margin-bottom: 0;
}
@@ -1095,204 +1156,200 @@ article.quote h2 {
@media screen and (min-width: 961px) {
- #topmenus a {
+ #top-menus a {
color: rgb(124, 124, 124);
}
- .calltoaction h2 a {
+ .call-to-action h2 a {
font-size: 20px;
}
.get-started-row {
display: flex;
}
- .mainheader {
+ .main-header {
padding-bottom: 50px;
}
- #showNavigationLink {
+ #show-navigation-link {
display: none;
}
- .homebar h1 {
+ .home-bar h1 {
font-size: 27px;
}
- .grailslogo {
+ .grails-logo {
float: left;
display: block;
}
- #topmenus {
+ #top-menus {
margin-top: 0;
display: block;
background-color: #ffffff;
}
- body.guide #topmenus,
- body.grails3plugins #topmenus,
- body.gorm #topmenus {
+ body.guide #top-menus,
+ body.grails3-plugins #top-menus,
+ body.gorm #top-menus {
background-color: #F5F5F5;
}
- .mainmenu li,
- .secondarymenu li,
- .secondarymenu li a,
- .mainmenu li a {
+ .main-menu li,
+ .secondary-menu li,
+ .secondary-menu li a,
+ .main-menu li a {
text-align: center;
}
- .mainmenu li,
- .secondarymenu li {
+ .main-menu li,
+ .secondary-menu li {
text-align: right;
margin-left: 25px;
}
- .mainmenu li { font-size: 16px; }
- .secondarymenu li { font-size: 10px; }
+ .main-menu li { font-size: 16px; }
+ .secondary-menu li { font-size: 10px; }
- .mainmenu li,
- .secondarymenu li {
+ .main-menu li,
+ .secondary-menu li {
padding-bottom: 11px;
border-bottom: 3px solid #FFFFFF;
}
- body.guide .mainmenu li,
- body.guide .secondarymenu li,
- body.grails3plugins .mainmenu li,
- body.grails3plugins .secondarymenu li,
- body.gorm .mainmenu li,
- body.gorm .secondarymenu li {
+ body.guide .main-menu li,
+ body.guide .secondary-menu li,
+ body.grails3-plugins .main-menu li,
+ body.grails3-plugins .secondary-menu li,
+ body.gorm .main-menu li,
+ body.gorm .secondary-menu li {
border-bottom: 3px solid #f5f5f5;
}
- body.home .mainmenu li,
- body.home .secondarymenu li {
+ body.home .main-menu li,
+ body.home .secondary-menu li {
border-bottom: 3px solid #FFFFFF;
}
- body.guide .mainmenu li.active,
- body.guide .secondarymenu li.active,
- body.grails3plugins .mainmenu li.active,
- body.grails3plugins .secondarymenu li.active,
- .mainmenu li.active,
- .secondarymenu li.active {
+ body.guide .main-menu li.active,
+ body.guide .secondary-menu li.active,
+ body.grails3-plugins .main-menu li.active,
+ body.grails3-plugins .secondary-menu li.active,
+ .main-menu li.active,
+ .secondary-menu li.active {
border-bottom: 3px solid #feb672;
}
- .secondarymenu li.active a,
- .mainmenu li.active a {
+ .secondary-menu li.active a,
+ .main-menu li.active a {
text-decoration: none;
}
- .secondarymenu,
- .mainmenu {
+ .secondary-menu,
+ .main-menu {
float: right;
}
- .secondarymenu li,
- .mainmenu li {
+ .secondary-menu li,
+ .main-menu li {
float: left;
width: auto;
}
- .secondarymenu li a,
- .mainmenu li a {
+ .secondary-menu li a,
+ .main-menu li a {
padding: 0;
}
- .secondarymenu {
+ .secondary-menu {
margin-bottom: 35px;
}
- .mainmenu li a {
+ .main-menu li a {
text-transform: none;
}
- .homebar {
+ .home-bar {
background-image: url(../images/banner.png);
background-repeat: no-repeat;
background-position: center top;
}
- .chalicesbg {
+ .chalices-bg {
background-image: url(../images/chalices.svg);
background-repeat: no-repeat;
background-position: center center;
}
- .chalicesbg a,
- .chalicesbg a:visited,
- .chalicesbg a:hover {
+ .chalices-bg a,
+ .chalices-bg a:visited,
+ .chalices-bg a:hover {
color: #FFFFFF;
text-decoration: none;
}
- .chalicesbg a:hover {
+ .chalices-bg a:hover {
text-decoration: underline;
}
- .headerbar {
+ .header-bar {
height: 200px;
padding-top: 0;
}
- .headerbar h1 {
+ .header-bar h1 {
margin-top: 110px;
}
- .twocolumns .column {
+ .two-columns .column {
float: right;
width: 47.32690622%; /* 547 / 1141px;*/
}
- .goldenratio .column {
+ .golden-ratio .column {
float: right;
width: 38.2%;
}
- .goldenratio .column:first-child {
+ .golden-ratio .column:first-child {
float: right;
width: 61.8%;
}
- .fivecolumns,
- .fourcolumns,
- .threecolumns {
+ .five-columns,
+ .four-columns,
+ .three-columns {
clear: both;
overflow: auto;
margin: 0;
padding: 0;
}
- .fivecolumns .column,
- .fourcolumns .column,
- .threecolumns .column {
+ .five-columns .column,
+ .four-columns .column,
+ .three-columns .column {
float: left;
overflow: auto;
}
- .fivecolumns .column {
+ .five-columns .column {
width: 18%;
}
- .fourcolumns .column {
+ .four-columns .column {
width: 23%;
}
- .threecolumns .column {
+ .three-columns .column {
width: 31%;
}
- .fivecolumns .column,
- .fourcolumns .column {
+ .five-columns .column,
+ .four-columns .column {
margin-left: 1%;
margin-right: 1%;
}
- .threecolumns .column:nth-child(3n+2) {
+ .three-columns .column:nth-child(3n+2) {
margin-left: 3%;
margin-right: 3%;
}
- .threecolumns .column:nth-child(3n+1) {
+ .three-columns .column:nth-child(3n+1) {
clear: left;
}
- .twocolumns .column.odd,
- .twocolumns .column:first-child {
+ .two-columns .column.odd,
+ .two-columns .column:first-child {
margin-left: 4.382120947%; /* 30px / 1141px; */
}
footer {
overflow: auto;
}
- footer .content {
- display: flex;
- justify-content: center;
- }
- .partnersnav li {
+ .partners-nav li {
text-align: right;
}
.event img {
@@ -1302,20 +1359,18 @@ article.quote h2 {
width: 120px;
}
- .calltoactions {
+ .call-to-actions {
width: 56.09114812%;/* 640px / 1141px; */
margin: 0 auto;
overflow: auto;
}
- .calltoaction {
+ .call-to-action {
width: 42.1875%; /* 270px / 640px */
float: left;
padding: 3.90625% 3.90625% 50px 3.90625%; /* 25px / 640px */
overflow: auto;
}
- .apache-grails span { text-align: left; }
- .apache-grails { text-align: left !important; }
}
/**************/
@@ -1342,7 +1397,7 @@ article.guide {
border-top-right-radius: 5px;
background-color: #ffffff !important;
}
-article.guide .mauticform_wrapper {
+article.guide .mautic-form-wrapper {
margin: 0;
}
@media screen and (min-width: 961px) {
@@ -1372,16 +1427,16 @@ article.guide aside button.btn-large {
border: none;
}
-article.guide aside .githublinks {
+article.guide aside .github-links {
margin: 5px 0;
overflow: auto;
}
-ul.githublinks {
+ul.github-links {
padding-left: 0;
}
-article.guide aside .githublinks li {
+article.guide aside .github-links li {
width: 46%;
margin-top: 0.5%;
margin-bottom: 0.5%;
@@ -1389,15 +1444,15 @@ article.guide aside .githublinks li {
list-style-type: none;
float: left;
}
-article.guide aside .githublinks li:first-child {
+article.guide aside .github-links li:first-child {
margin-right: 4%;
margin-left: 0;
}
-article.guide aside .githublinks li:last-child {
+article.guide aside .github-links li:last-child {
margin-left: 4%;
margin-right: 0;
}
-article.guide aside .githublinks li button {
+article.guide aside .github-links li button {
width: 100%;
}
@@ -1413,8 +1468,8 @@ article.guide aside .githublinks li button {
article.guide th {
color: #ffffff;
}
-article.guide #ocitraining td,
-article.guide td.tableblock {
+article.guide #oci-training td,
+article.guide td.table-block {
background-color: rgb(245, 245, 245);
}
article.guide td a {
@@ -1431,8 +1486,8 @@ article.guide h3,
article.guide h4,
article.guide h5,
article.guide h6,
-article.guide #toctitle,
-article.guide .sidebarblock>.content>.title {
+article.guide #toc-title,
+article.guide .sidebar-block>.content>.title {
color: #7c7c7c;
}
@@ -1474,33 +1529,33 @@ article.guide .tip td.content {
}
-body.grails3plugins .thirdmenu ul {
+body.grails3-plugins .third-menu ul {
margin: 0;
padding: 0;
}
-body.grails3plugins .thirdmenu li {
+body.grails3-plugins .third-menu li {
list-style-type: none;
border-bottom: 1px solid #eee;
border-left: 4px solid #ffffff !important;
}
-body.grails3plugins .thirdmenu li.active {
+body.grails3-plugins .third-menu li.active {
border-left: 4px solid #feb672 !important;
}
-body.grails3plugins .thirdmenu li a {
+body.grails3-plugins .third-menu li a {
color: #000000;
text-decoration: none;
padding: 20px;
display: block;
}
-body.grails3plugins article {
+body.grails3-plugins article {
margin-top: 0;
padding-top: 0;
background-color: #ffffff;
}
-.twocolumns .column .plugins:first-child {
+.two-columns .column .plugins:first-child {
margin-top: 20px;
}
.plugins-list {
@@ -1517,10 +1572,10 @@ body.grails3plugins article {
overflow: auto;
}
-.plugins-list li .iconlinks {
+.plugins-list li .icon-links {
float: right;
}
-.plugins-list li .iconlinks li {
+.plugins-list li .icon-links li {
list-style: none;
display: inline;
border: none;
@@ -1528,20 +1583,20 @@ body.grails3plugins article {
padding: 0;
}
-body.grails3plugins .labels {
+body.grails3-plugins .labels {
overflow: auto;
margin: 20px 0;
padding: 0;
clear: both;
}
-body.grails3plugins li.label {
+body.grails3-plugins li.label {
display: block;
float: left;
list-style-type: none;
margin: 5px;
padding: 0;
}
-body.grails3plugins li.label a {
+body.grails3-plugins li.label a {
border: 1px solid rgb(211, 211, 211);
text-decoration: none;
padding: 10px;
@@ -1573,10 +1628,10 @@ body.grails3plugins li.label a {
text-decoration: none;
}
-.align_left {
+.align-left {
float: left;
}
-.align_right {
+.align-right {
float: right;
}
@@ -1615,7 +1670,7 @@ body.grails3plugins li.label a {
cursor: pointer;
}
-.githubstar {
+.github-star {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
border-collapse: collapse;
@@ -1641,19 +1696,19 @@ body.grails3plugins li.label a {
border: 3px solid #DFDFDF;
}
-.githubstar a {
+.github-star a {
color: #000000;
display: block;
}
-.githubstar,
-.githubstar a {
+.github-star,
+.github-star a {
width: 120px;
height: 44px;
}
-.githubstar span.star,
-.githubstar span.count {
+.github-star span.star,
+.github-star span.count {
text-align: center;
display: block;
float: left;
@@ -1661,7 +1716,7 @@ body.grails3plugins li.label a {
height: 28px;
}
-.githubstar span.count {
+.github-star span.count {
width: 35px;
float: right;
border-left: 3px solid #DFDFDF;
@@ -1670,7 +1725,7 @@ body.grails3plugins li.label a {
margin-right: 1px;
height: 26px;
}
-.githubstar span.star {
+.github-star span.star {
width: 60px;
padding-top: 12px;
}
@@ -1684,19 +1739,19 @@ body.grails3plugins li.label a {
padding: 50px;
}
}
-body.post .largegoldenratio {
+body.post .large-golden-ratio {
max-width: 100%;
}
@media screen and (min-width: 961px) {
- .largegoldenratio {
+ .large-golden-ratio {
width: 56.45721134%;
margin-right: 5.34618756%;
}
- .smallgoldenratio {
+ .small-golden-ratio {
width: 38.1966011%;
}
}
-.blogpost h1 {
+.blog-post h1 {
font-size: 2em !important;
line-height: 1.5em !important;
margin-bottom: 0;
@@ -1705,32 +1760,32 @@ body.post .largegoldenratio {
font-family: 'Roboto-Medium', "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
text-transform: uppercase;
}
-.blogpost .date,
-.blogpost .author {
+.blog-post .date,
+.blog-post .author {
text-transform: uppercase;
font-family: 'Roboto-Medium', "Open Sans","Helvetica Neue",Helvetica,Arial, sans-serif;
}
-.light h2, .blogpost h2, .blogpost h2 {
+.light h2, .blog-post h2, .blog-post h2 {
font-family: 'Roboto-Medium', "Open Sans","Helvetica Neue",Helvetica,Arial, sans-serif;
text-transform: uppercase;
}
-.blogpost article {
+.blog-post article {
margin: 15px 0;
}
-.blogpost img {
+.blog-post img {
width: 100%;
}
@media screen and (min-width: 961px) {
- .blogcard h3 {
+ .blog-card h3 {
padding-top: 12.843%;
padding-bottom: 12.843%;
}
}
-.blogcard h3, .blogcard h2, .blogcard .tagsbytopic {
+.blog-card h3, .blog-card h2, .blog-card .tags-by-topic {
padding-left: 5.137%;
padding-right: 5.137%;
}
-.blogcard h3 {
+.blog-card h3 {
font-weight: normal;
font-family: 'Roboto-Medium', "Open Sans","Helvetica Neue",Helvetica,Arial, sans-serif;
}
@@ -1741,11 +1796,11 @@ body.post .largegoldenratio {
color: #231F20;
}
-.related-posts .blogcard:first-of-type {
+.related-posts .blog-card:first-of-type {
margin-top: 1rem !important;
}
@media screen and (min-width: 961px) {
- .blogcard {
+ .blog-card {
width: 88.77245509%;
height: 0;
margin-left: 11.22754491%;
@@ -1753,28 +1808,28 @@ body.post .largegoldenratio {
margin-top: 5.613772455%;
margin-bottom: 5.613772455%;
}
- .light .blogcard {
+ .light .blog-card {
background-color: #255AA8;
color: #FFFFFF;
}
- .light .blogcard a:visited,
- .light .blogcard a:hover,
- .light .blogcard a {
+ .light .blog-card a:visited,
+ .light .blog-card a:hover,
+ .light .blog-card a {
color: #FFFFFF;
text-decoration: none;
}
- .light .blogcard a:hover {
+ .light .blog-card a:hover {
text-decoration: underline;
}
}
@media screen and (max-width: 960px) {
- .blogcard {
+ .blog-card {
background-image: none !important;
margin-bottom: 50px;
}
}
@media screen and (min-width: 961px) {
- .container .smallgoldenratio .blogcard {
+ .container .small-golden-ratio .blog-card {
margin-left: 0;
margin-right: 11.22754491%;
margin-top: 9.585%;
@@ -1784,7 +1839,7 @@ body.post .largegoldenratio {
.padded-t h2 {
display: none;
}
-.padded-t .blogcard h2 {
+.padded-t .blog-card h2 {
display: block;
}
@@ -1796,10 +1851,10 @@ body.post .largegoldenratio {
padding-top: 50px;
overflow: auto;
}
- .blogcard {
+ .blog-card {
background-size: cover;
}
- .container .smallgoldenratio .blogcard:last-child {
+ .container .small-golden-ratio .blog-card:last-child {
margin-bottom: 9.585%;
}
}
@@ -1845,10 +1900,10 @@ li.plugin {
padding: 1px 10px 1px 15px;
}
-li.plugin .iconlinks {
+li.plugin .icon-links {
float: right;
}
-li.plugin .iconlinks li {
+li.plugin .icon-links li {
list-style: none;
display: inline;
border: none;
@@ -1885,12 +1940,12 @@ ul.labels li.label a {
text-transform: uppercase;
}
-li.plugin.p{
+li.plugin.p {
font-size: 16px;
line-height: 20px;
}
-.versionselector select {
+.version-selector select {
border-color: #ddd;
border-radius: 2px;
}
\ No newline at end of file
diff --git a/assets/stylesheets/support-schedule.css b/assets/stylesheets/support-schedule.css
index f7eb0ab0417..d9d9c9c0f74 100644
--- a/assets/stylesheets/support-schedule.css
+++ b/assets/stylesheets/support-schedule.css
@@ -3,7 +3,7 @@ table.support-schedule {
overflow-x: auto;
}
-.twocolumns.support-schedule {
+.two-columns.support-schedule {
background-color: #ffffff;
}
diff --git a/assets/stylesheets/support.css b/assets/stylesheets/support.css
index d7ab8654fbd..3fcfe9a8aa5 100644
--- a/assets/stylesheets/support.css
+++ b/assets/stylesheets/support.css
@@ -235,7 +235,7 @@ table.support-schedule {
overflow-x: auto;
}
-.twocolumns.support-schedule {
+.two-columns.support-schedule {
background-color: #ffffff;
}
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 60f50c9432a..a76806a10d6 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -45,7 +45,7 @@ gradlePlugin {
plugins {
register('grails-website') {
id = 'grails-website'
- implementationClass = 'org.grails.gradle.GrailsWebsitePlugin'
+ implementationClass = 'website.gradle.GrailsWebsitePlugin'
displayName = 'Grails Website Plugin'
description = 'Creates tasks to generate the Grails Website'
}
diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/buildSrc/src/main/groovy/org/grails/Content.groovy b/buildSrc/src/main/groovy/org/grails/Content.groovy
deleted file mode 100644
index c3f6cdc8843..00000000000
--- a/buildSrc/src/main/groovy/org/grails/Content.groovy
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.grails
-
-import edu.umd.cs.findbugs.annotations.Nullable
-
-interface Content {
- @Nullable String getPath();
- @Nullable String getTitle();
- /**
- *
- * @return Body class
- */
- @Nullable String getBody();
- @Nullable String getDate();
- @Nullable String getDescription();
- @Nullable List<String> getKeywords();
- @Nullable String getRobots();
-}
diff --git a/buildSrc/src/main/groovy/org/grails/ContentAndMetadata.groovy b/buildSrc/src/main/groovy/org/grails/ContentAndMetadata.groovy
deleted file mode 100644
index 5bb001a5d02..00000000000
--- a/buildSrc/src/main/groovy/org/grails/ContentAndMetadata.groovy
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.grails
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class ContentAndMetadata {
- String content
- Map<String, String> metadata
-}
diff --git a/buildSrc/src/main/groovy/org/grails/HtmlMinutes.groovy b/buildSrc/src/main/groovy/org/grails/HtmlMinutes.groovy
deleted file mode 100644
index 6bd3c6f3ca0..00000000000
--- a/buildSrc/src/main/groovy/org/grails/HtmlMinutes.groovy
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.grails
-
-class HtmlMinutes {
- String path
- MinutesMetadata metadata
- String html
-}
diff --git a/buildSrc/src/main/groovy/org/grails/HtmlPost.groovy b/buildSrc/src/main/groovy/org/grails/HtmlPost.groovy
deleted file mode 100644
index 98cf405546f..00000000000
--- a/buildSrc/src/main/groovy/org/grails/HtmlPost.groovy
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.grails
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class HtmlPost {
-
- String path
- PostMetadata metadata
- String html
- Set<String> tags
-}
diff --git a/buildSrc/src/main/groovy/org/grails/MinutesMetadata.groovy b/buildSrc/src/main/groovy/org/grails/MinutesMetadata.groovy
deleted file mode 100644
index d4bd49f2a1d..00000000000
--- a/buildSrc/src/main/groovy/org/grails/MinutesMetadata.groovy
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.grails
-
-import edu.umd.cs.findbugs.annotations.Nullable
-import groovy.transform.CompileStatic
-
-@CompileStatic
-interface MinutesMetadata {
-
- String get(String name)
-
- @Nullable
- String getUrl()
-
- @Nullable
- String getTitle()
-
- @Nullable
- String getDate()
-
- Map<String, String> toMap()
-
-}
diff --git a/buildSrc/src/main/groovy/org/grails/MinutesMetadataAdaptor.groovy b/buildSrc/src/main/groovy/org/grails/MinutesMetadataAdaptor.groovy
deleted file mode 100644
index 3bfd7ef28e0..00000000000
--- a/buildSrc/src/main/groovy/org/grails/MinutesMetadataAdaptor.groovy
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.grails
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class MinutesMetadataAdaptor implements MinutesMetadata {
-
- Map<String, String> metadata
- MinutesMetadataAdaptor(Map<String, String> metadata) {
- this.metadata = metadata
-
- }
-
- @Override
- String get(String name) {
- return metadata[name]
- }
-
- @Override
- String getUrl() {
- get('url')
- }
-
- @Override
- String getTitle() {
- get('title')
- }
-
- @Override
- String getDate() {
- get('date')
- }
-
- @Override
- Map<String, String> toMap() {
- metadata
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/PageElement.groovy b/buildSrc/src/main/groovy/org/grails/PageElement.groovy
deleted file mode 100644
index 9f7ec8e2390..00000000000
--- a/buildSrc/src/main/groovy/org/grails/PageElement.groovy
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.grails
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-interface PageElement {
-
- String renderAsHtml()
-}
diff --git a/buildSrc/src/main/groovy/org/grails/PostMetadata.groovy b/buildSrc/src/main/groovy/org/grails/PostMetadata.groovy
deleted file mode 100644
index c12f68a16d6..00000000000
--- a/buildSrc/src/main/groovy/org/grails/PostMetadata.groovy
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.grails
-
-import jakarta.annotation.Nullable
-
-interface PostMetadata {
-
- String get(String name)
-
- @Nullable
- String getUrl()
-
- @Nullable
- String getTitle()
-
- @Nullable
- String getAuthor()
-
- @Nullable
- String getDate()
-
- Map<String, String> toMap()
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/org/grails/PostMetadataAdapter.groovy b/buildSrc/src/main/groovy/org/grails/PostMetadataAdapter.groovy
deleted file mode 100644
index c9a50bcda99..00000000000
--- a/buildSrc/src/main/groovy/org/grails/PostMetadataAdapter.groovy
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.grails
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class PostMetadataAdapter implements PostMetadata {
-
- Map<String, String> metadata
- PostMetadataAdapter(Map<String, String> metadata) {
- this.metadata = metadata
-
- }
-
- @Override
- String get(String name) {
- return metadata[name]
- }
-
- @Override
- String getUrl() {
- get('url')
- }
-
- @Override
- String getTitle() {
- get('title')
- }
-
- @Override
- String getAuthor() {
- get('author')
- }
-
- @Override
- String getDate() {
- get('date')
- }
-
- @Override
- Map<String, String> toMap() {
- metadata
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/documentation/DocumentationPage.groovy b/buildSrc/src/main/groovy/org/grails/documentation/DocumentationPage.groovy
deleted file mode 100644
index e46b8f1e418..00000000000
--- a/buildSrc/src/main/groovy/org/grails/documentation/DocumentationPage.groovy
+++ /dev/null
@@ -1,167 +0,0 @@
-package org.grails.documentation
-
-import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import groovy.xml.MarkupBuilder
-import org.yaml.snakeyaml.Yaml
-
-@CompileStatic
-class DocumentationPage {
-
- @CompileDynamic
- private static String renderCategory(DocumentationCategory cat) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div(class: 'guidegroup') {
- html.div(class: 'guidegroupheader') {
- html.img(src: cat.image, alt: cat.title)
- h2 cat.title
- }
- ul {
- cat.items.each { item ->
- li {
- a(href: item.url, item.title)
- }
- }
- }
- }
- writer.toString()
- }
-
- @CompileDynamic
- static String renderDocumentation(String version) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div(class: "guidegroup") {
- if (version) {
- boolean isSnapshot = version.endsWith('-SNAPSHOT') || version.contains('snapshot')
- div(class: "guidegroupheader") {
- img(src: "[%url]/images/documentation.svg", alt: "Grails Version (${version})")
- h2 "${isSnapshot ? 'Snapshot' : (version.contains('.M') ? 'Milestone' : (version.contains('.RC') ? 'Release Candidate': 'Latest'))} Version (${version}) Documentation"
- }
- ul {
- li {
- a(href: "https://grails.apache.org/docs/${version}/", 'User Guide')
- }
- li {
- a(href: "https://grails.apache.org/docs/${version}/api/", 'API Reference')
- }
- }
- }
- }
- writer.toString()
- }
-
-
- @CompileDynamic
- static String mainContent(File releases, File modules) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
-
- html.div(class: 'headerbar chalicesbg') {
- html.div(class: 'content') {
- h1 'Documentation'
- }
- }
- SoftwareVersion preRelease = SiteMap.latestPreReleaseVersion(releases)
- SoftwareVersion latest = SiteMap.latestVersion(releases)
- Collection<DocumentationCategory> categories = categories(modules)
- html.div(class: 'content') {
- html.div(class: "twocolumns") {
- html.div(class: "odd column"){
- if (preRelease > latest) {
- mkp.yieldUnescaped(renderDocumentation(preRelease.versionText))
- }
-
- mkp.yieldUnescaped(renderDocumentation('snapshot'))
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'GORM - Data Access Toolkit' }))
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'Security' }))
-
- }
- html.div(class: "column") {
- mkp.yieldUnescaped(renderDocumentation(latest.versionText))
-
- html.div(class: 'olderversions') {
- h3(class: 'columnheader', style: 'margin-bottom: 10px;', 'Older Version')
- p 'Browse previous versions\' documentation since Grails 1.2.0'
-
- div(class: 'versionselector') {
- h4 'Single Page - User Guide'
- select(onchange: "window.location.href='https://grails.apache.org/docs/' + this.value + '/guide/single.html'") {
- option 'Select a version'
- mkp.yield('[%versions]')
- }
- }
- div(class: 'versionselector') {
- h4 'User Guide'
- select(onchange: "window.location.href='https://grails.apache.org/docs/' + this.value") {
- option 'Select a version'
- mkp.yield('[%versions]')
- }
- }
- div(class: 'versionselector') {
- h4 'API Reference'
- select(onchange: "window.location.href='https://grails.apache.org/docs/' + this.value + '/api'") {
- option 'Select a version'
- mkp.yield('[%versions]')
- }
- }
- }
-
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'Upgrade' }))
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'Testing' }))
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'Views' }))
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'Async' }))
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'Database' }))
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'Redis' }))
- }
-
- }
- }
-
- writer.toString()
- }
-
- @CompileDynamic
- private static Collection<DocumentationCategory> categories(File modules) {
- Yaml yaml = new Yaml()
- Map model = yaml.load(modules.newDataInputStream())
- Map<String, DocumentationCategory> categories = [:]
- model['modules'].collect { k, v ->
- if (!categories.containsKey(v.category)) {
- DocumentationCategory cat = new DocumentationCategory()
- if (v.category) {
- cat.title = v.category
- }
- if (v.categoryImage) {
- cat.image = v.categoryImage
- }
- if (v.categoryDescription) {
- cat.description = v.categoryDescription
- }
- if (v.categoryUrl) {
- cat.url = v.categoryUrl
- }
- categories[v.category] = cat
- }
- DocumentationItem item = new DocumentationItem()
- item.url = v.url
- item.title = v.title
- categories[v.category].items << item
- }
- categories.values()
- }
-
- static class DocumentationCategory {
- String title
- String image
- String description
- String url
- List<DocumentationItem> items = []
- }
-
- static class DocumentationItem {
- String title
- String url
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/documentation/DownloadPage.groovy b/buildSrc/src/main/groovy/org/grails/documentation/DownloadPage.groovy
deleted file mode 100644
index 17c0d7b168a..00000000000
--- a/buildSrc/src/main/groovy/org/grails/documentation/DownloadPage.groovy
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.grails.documentation
-
-import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import groovy.xml.MarkupBuilder
-
-@CompileStatic
-class DownloadPage {
-
- static String binaryUrl(String version, String artifact = 'grails', String ext = '', String directory = 'core') {
- "https://www.apache.org/dyn/closer.lua/grails/${directory}/${version}/distribution/apache-${artifact}-${version}-bin.zip${ext}?action=download"
- }
-
- static String sourceUrl(String version, String artifact = 'grails', String ext = '', String directory = 'core') {
- "https://www.apache.org/dyn/closer.lua/grails/${directory}/${version}/sources/apache-${artifact}-${version}-src.zip${ext}?action=download"
- }
-
- static String binaryVerificationUrl(String version, String artifact = 'grails', String ext = '', String directory = 'core') {
- "https://downloads.apache.org/grails/${directory}/${version}/distribution/apache-${artifact}-${version}-bin.zip${ext}"
- }
-
- static String sourceVerificationUrl(String version, String artifact = 'grails', String ext = '', String directory = 'core') {
- "https://downloads.apache.org/grails/${directory}/${version}/sources/apache-${artifact}-${version}-src.zip${ext}"
- }
-
- /**
- * Does not handle pre-release versions as these are not displayed in the select box.
- */
- static String resolveOldDownloadUrl(String version) {
- def parts = ((version.split(/\./)*.replaceAll(/\D.*/, '')*.toInteger()) + [0, 0, 0]).take(3)
- def (major, minor, patch) = [parts[0], parts[1], parts[2]]
- def tag = "v$version"
- if (major < 7) {
- def baseUrl = 'https://github.com/apache/grails-core/releases/download'
- def artifactName = "grails-$version"
- if (major == 6) {
- baseUrl = 'https://github.com/apache/grails-forge/releases/download'
- artifactName = "grails-cli-$version"
- }
- if (major == 1 && minor == 1) {
- artifactName = "grails-bin-$version"
- }
- if (major <= 1 && patch == 0) {
- tag = "v$major.$minor"
- }
- return "$baseUrl/$tag/${artifactName}.zip"
- }
- else {
- return "https://www.apache.org/dyn/closer.lua/grails/core/$version/distribution/apache-grails-$version-bin.zip?action=download"
- }
- }
-
- @CompileDynamic
- static String renderDownload(String version) {
- String redisVersion = '5.0.0'
- String quartzVersion = '4.0.0'
- String springSecurityVersion = '7.0.0'
- String grailsGithubActionsVersion = '1.0.1'
- String grailsGradlePublishVersion = '0.0.4'
-
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- boolean isSnapshot = version.endsWith('-SNAPSHOT') || version.contains('snapshot')
- if (!isSnapshot) {
- html.div(class: "guidegroup") {
- if (version) {
- div(class: "guidegroupheader") {
- img(src: "[%url]/images/download.svg", alt: "Download Grails (${version})")
- h2 "${isSnapshot ? 'Snapshot' : (version.contains('-M') ? 'Milestone' : (version.contains('-RC') ? 'Release Candidate': 'Latest Stable'))} Version (${version}) Downloads"
- }
- ul {
- if (version.startsWith('7')) {
- li {
- a(href: sourceUrl(version), 'Source')
- a(href: sourceVerificationUrl(version, 'grails', '.sha512'), 'SHA512')
- a(href: sourceVerificationUrl(version, 'grails', '.asc'), 'ASC')
- }
- li {
- a(href: binaryUrl(version, 'grails'), 'Binary')
- a(href: binaryVerificationUrl(version, 'grails', '.sha512'), 'SHA512')
- a(href: binaryVerificationUrl(version, 'grails', '.asc'), 'ASC')
- }
- li {
- a(href: binaryUrl(version, 'grails-wrapper'), 'Binary Wrapper')
- a(href: binaryVerificationUrl(version, 'grails-wrapper', '.sha512'), 'SHA512')
- a(href: binaryVerificationUrl(version, 'grails-wrapper', '.asc'), 'ASC')
- }
- li {
- a(href: sourceUrl(springSecurityVersion, 'grails-spring-security', '', 'spring-security'), "Grails Spring Security ${springSecurityVersion} Plugin Source")
- a(href: sourceVerificationUrl(springSecurityVersion, 'grails-spring-security', '.sha512', 'spring-security'), 'SHA512')
- a(href: sourceVerificationUrl(springSecurityVersion, 'grails-spring-security', '.asc', 'spring-security'), 'ASC')
- }
- li {
- a(href: sourceUrl(redisVersion, 'grails-redis', '', 'redis'), "Grails Redis ${redisVersion} Plugin Source")
- a(href: sourceVerificationUrl(redisVersion, 'grails-redis', '.sha512', 'redis'), 'SHA512')
- a(href: sourceVerificationUrl(redisVersion, 'grails-redis', '.asc', 'redis'), 'ASC')
- }
- li {
- a(href: sourceUrl(quartzVersion, 'grails-quartz', '', 'quartz'), "Grails Quartz ${quartzVersion} Plugin Source")
- a(href: sourceVerificationUrl(quartzVersion, 'grails-quartz', '.sha512', 'quartz'), 'SHA512')
- a(href: sourceVerificationUrl(quartzVersion, 'grails-quartz', '.asc', 'quartz'), 'ASC')
- }
- li {
- a(href: sourceUrl(grailsGithubActionsVersion, 'grails-github-actions', '', 'actions'), "Grails GitHub Actions ${grailsGithubActionsVersion} Source")
- a(href: sourceVerificationUrl(grailsGithubActionsVersion, 'grails-github-actions', '.sha512', 'actions'), 'SHA512')
- a(href: sourceVerificationUrl(grailsGithubActionsVersion, 'grails-github-actions', '.asc', 'actions'), 'ASC')
- }
- li {
- a(href: sourceUrl(grailsGradlePublishVersion, 'grails-publish', '', 'grails-publish'), "Grails Publish Gradle Plugin ${grailsGradlePublishVersion} Source")
- a(href: sourceVerificationUrl(grailsGradlePublishVersion, 'grails-publish', '.sha512', 'grails-publish'), 'SHA512')
- a(href: sourceVerificationUrl(grailsGradlePublishVersion, 'grails-publish', '.asc', 'grails-publish'), 'ASC')
- }
- } else {
- li {
- a(href: "https://github.com/apache/grails-forge/releases/download/v${version}/grails-cli-${version}.zip", 'Binary')
- }
- }
- li {
- a(href: "https://github.com/apache/grails-core/releases/tag/v${version}", 'Grails Release Notes')
- }
- if (version.startsWith('7')) {
- li {
- a(href: "https://github.com/apache/grails-spring-security/releases/tag/v${springSecurityVersion}", "Grails Spring Security Plugin ${springSecurityVersion} Release Notes")
- }
- li {
- a(href: "https://github.com/apache/grails-redis/releases/tag/v${redisVersion}", "Grails Redis ${redisVersion} Plugin Release Notes")
- }
- li {
- a(href: "https://github.com/apache/grails-quartz/releases/tag/v${quartzVersion}", "Grails Quartz ${quartzVersion} Plugin Release Notes")
- }
- li {
- a(href: "https://github.com/apache/grails-github-actions/releases/tag/v${grailsGithubActionsVersion}", "Grails GitHub Actions ${grailsGithubActionsVersion} Release Notes")
- }
- li {
- a(href: "https://github.com/apache/grails-gradle-publish/releases/tag/v${grailsGradlePublishVersion}", "Grails Publish Gradle Plugin ${grailsGradlePublishVersion} Release Notes")
- }
- }
- }
- }
- }
- }
- writer.toString()
- }
-
-
- @CompileDynamic
- static String mainContent(File releases) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
-
- html.div(class: 'headerbar chalicesbg') {
- html.div(class: 'content') {
- h1 'Downloads'
- }
- }
- SoftwareVersion preRelease = SiteMap.latestPreReleaseVersion(releases)
- SoftwareVersion latest = SiteMap.latestVersion(releases)
- html.div(class: 'content') {
- html.div(class: "twocolumns") {
- html.div(class: "odd column"){
- h3(class: 'columnheader', style: 'margin-bottom: 10px;', 'Source and Binary Releases')
- mkp.yieldUnescaped "We provide OpenPGP signatures ('.asc') files and checksums ('.sha512') for every release artifact. We recommend that you "
- a(href: 'https://www.apache.org/info/verification.html', 'verify')
- mkp.yieldUnescaped " the integrity of downloaded files by generating your own checksums and match them against ours, and checking signatures using the "
- a(href: 'https://www.apache.org/dyn/closer.lua/grails/KEYS?action=download', 'KEYS')
- mkp.yieldUnescaped " file which contains the Grails OpenPGP release keys."
- p ''
- if (preRelease > latest) {
- mkp.yieldUnescaped(renderDownload(preRelease.versionText))
- }
-
- mkp.yieldUnescaped(renderDownload('snapshot'))
-
- mkp.yieldUnescaped(renderDownload(latest.versionText))
-
- h3(class:'columnheader', 'Older Versions')
- p 'You can download previous versions as far back as Grails 0.1.'
- p 'NOTE: Versions prior to 7.0.0-M4 are not ASF releases. Links to those releases are provided here as a convenience.'
- div(class: 'versionselector') {
- select(class: 'form-control', onchange: 'window.location.href=this.value') {
- option(label: 'Select a version', disabled: 'disabled', selected: 'selected')
- SiteMap.stableVersions(releases)*.versionText.each {
- option(value: resolveOldDownloadUrl(it), it)
- }
- }
- }
- }
- html.div(class: "column") {
-
- h3(class: 'columnheader', style: 'margin-bottom: 10px;', 'Grails Application Forge')
-
- mkp.yieldUnescaped'The quickest way to get started with our application generator:'
- p {
- a(href: 'https://start.grails.org', 'Grails Application Forge')
- }
-
-
- h3(class: 'columnheader', style: 'margin-bottom: 10px;', 'Installing with SDKMAN!')
-
- p {
- a(href: 'https://sdkman.io/', 'SDKMAN! (The Software Development Kit Manager)')
- }
-
- p 'This tool makes installing the Grails framework on any Unix based platform (Mac OSX, Linux, Cygwin, Solaris, or FreeBSD) easy.'
- p 'Simply open a new terminal and enter:'
- div(class:'code', '$ curl -s https://get.sdkman.io | bash')
- p 'Follow the on-screen instructions to complete installation.'
- p 'Open a new terminal or type the command:'
- div(class:'code', '$ source "$HOME/.sdkman/bin/sdkman-init.sh"')
- p 'Then install the latest stable Grails version:'
- div(class:'code', '$ sdk install grails')
- p 'If prompted, make this your default version. After installation is complete it can be tested with:'
- div(class:'code', '$ grails --version')
- p "That's all there is to it!"
- }
- }
- }
-
- writer.toString()
- }
-
-}
diff --git a/buildSrc/src/main/groovy/org/grails/documentation/GuideGroup.groovy b/buildSrc/src/main/groovy/org/grails/documentation/GuideGroup.groovy
deleted file mode 100644
index b0383827cac..00000000000
--- a/buildSrc/src/main/groovy/org/grails/documentation/GuideGroup.groovy
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.grails.documentation
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class GuideGroup implements GuideGroupHtml {
- String title
- String image
- String description
- String ulId
- List<GuideGroupItem> items = []
-}
diff --git a/buildSrc/src/main/groovy/org/grails/documentation/GuideGroupHtml.groovy b/buildSrc/src/main/groovy/org/grails/documentation/GuideGroupHtml.groovy
deleted file mode 100644
index 15eb10d6cd3..00000000000
--- a/buildSrc/src/main/groovy/org/grails/documentation/GuideGroupHtml.groovy
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.grails.documentation
-
-import groovy.transform.CompileDynamic
-import groovy.xml.MarkupBuilder
-import org.grails.PageElement
-
-trait GuideGroupHtml implements PageElement {
-
- abstract String getUlId()
-
- abstract String getImage()
-
- abstract String getTitle()
-
- abstract List<GuideGroupItem> getItems()
-
- abstract String getDescription()
-
- @CompileDynamic
- @Override
- String renderAsHtml() {
- renderAsHtmlWithStyleAttr(null)
- }
-
- String renderAsHtmlWithStyleAttr(String styleAttr) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- final int imageMarginRight = 10
- html.div(class: "guidegroup", style: "${styleAttr ?: ''}") {
- div(class: "guidegroupheader") {
- img src: image, alt: title
- h2 {
- html.mkp.yieldUnescaped title
- }
- }
- ul(id: getUlId() ?: '') {
- if ( description ) {
- li(class: 'legend', description)
- }
- if ( items ) {
- for ( GuideGroupItem item : items ) {
- if ( item.href ) {
- li {
- if (item.image) {
- img width: 70, class: 'align-left', style: "margin-right: ${imageMarginRight}px;", src: "${item.image}" as String, alt: item.title
- }
- a href: item.href, item.title
- if (item.legend) {
- p(item.legend)
- }
- }
- } else {
- li(item.title)
- }
- }
- }
- }
- }
- writer.toString()
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/documentation/GuideGroupItem.groovy b/buildSrc/src/main/groovy/org/grails/documentation/GuideGroupItem.groovy
deleted file mode 100644
index e0dce3346fb..00000000000
--- a/buildSrc/src/main/groovy/org/grails/documentation/GuideGroupItem.groovy
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.grails.documentation
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class GuideGroupItem {
- String title
- String href
- String legend
- String image
- String category
-}
diff --git a/buildSrc/src/main/groovy/org/grails/documentation/ProfilesPage.groovy b/buildSrc/src/main/groovy/org/grails/documentation/ProfilesPage.groovy
deleted file mode 100644
index be366d30817..00000000000
--- a/buildSrc/src/main/groovy/org/grails/documentation/ProfilesPage.groovy
+++ /dev/null
@@ -1,104 +0,0 @@
-package org.grails.documentation;
-
-import groovy.transform.CompileDynamic;
-import groovy.transform.CompileStatic;
-import groovy.xml.MarkupBuilder
-import org.grails.utils.MarkdownUtils
-import org.yaml.snakeyaml.Yaml;
-
-@CompileStatic
-class ProfilesPage {
-
- @CompileDynamic
- static String mainContent(File profiles) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div( class:'headerbar chalicesbg'){
- div( class:'content'){
- h1 'Profiles'
- }
- }
- Collection<ProfilesCategory> categories = categories(profiles)
- html.div(class: 'content') {
- div(class: 'twocolumns') {
- div(class: 'odd column') {
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'Profiles' }))
- }
- div(class: 'column') {
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'Plugin Profiles' }))
- mkp.yieldUnescaped(renderCategory(categories.find { it.title == 'Third-Party Profiles' }))
-
- }
- }
- }
- writer.toString()
- }
-
- @CompileDynamic
- private static Collection<ProfilesCategory> categories(File modules) {
- Yaml yaml = new Yaml()
- Map model = yaml.load(modules.newDataInputStream())
- Map<String, ProfilesCategory> categories = [:]
- model['profiles'].collect { k, v ->
- if (!categories.containsKey(v.category)) {
- ProfilesCategory cat = new ProfilesCategory()
- if (v.category) {
- cat.title = v.category
- }
- if (v.image) {
- cat.image = v.image
- }
- if (v.description) {
- cat.description = v.description
- }
- categories[v.category] = cat
- }
- Profile item = new Profile()
- item.url = v.url
- item.title = v.title
- categories[v.category].profiles << item
- }
- categories.values()
- }
-
- @CompileDynamic
- private static String renderCategory(ProfilesCategory cat) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- if (cat) {
- html.div(class: 'guidegroup') {
- div(class: 'guidegroupheader') {
- html.img(src: cat.image, alt: cat.title)
- h2 cat.title
- }
- ul {
- if (cat.description) {
- li(class: 'legend') {
- mkp.yieldUnescaped MarkdownUtils.htmlFromMarkdown(cat.description)
- .replaceAll('<p>', '')
- .replaceAll('</p>', '')
- }
- }
- cat.profiles.each { item ->
- li {
- a(href: item.url, item.title)
- }
- }
- }
- }
- }
- writer.toString()
- }
-
- static class ProfilesCategory {
- String title
- String image
- String description
- List<Profile> profiles = []
- }
-
- static class Profile {
- String title
- String url
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/documentation/SiteMap.groovy b/buildSrc/src/main/groovy/org/grails/documentation/SiteMap.groovy
deleted file mode 100644
index 69359de69a9..00000000000
--- a/buildSrc/src/main/groovy/org/grails/documentation/SiteMap.groovy
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.grails.documentation
-
-import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import org.yaml.snakeyaml.Yaml
-
-@CompileStatic
-class SiteMap {
-
- static List<SoftwareVersion> versions(File releases) {
- Yaml yaml = new Yaml()
- assert releases.exists()
- Map model = yaml.load(releases.newDataInputStream())
- model['releases'].collect { Map release ->
- SoftwareVersion.build(release['version'] as String)
- }.sort()
- }
-
- static SoftwareVersion latestVersion(File releases) {
- List<SoftwareVersion> versions = stableVersions(releases)
- versions ? versions.get(0) : null
- }
-
- static List<String> olderVersions(File releases) {
- stableVersions(releases).reverse().drop(1).collect { it.versionText }
- }
-
- static List<String> versionsBeforeGrails6(File releases) {
- stableVersions(releases)
- .findAll { version -> version.major < 6 }
- .reverse()
- .drop(1)
- .collect { it.versionText }
- }
-
- static List<String> versionsAfterGrails6(File releases) {
- stableVersions(releases)
- .findAll { version -> version.major >= 6 }
- .reverse()
- .drop(1)
- .collect { it.versionText }
- }
-
- static List<SoftwareVersion> stableVersions(File releases) {
- (versions(releases).findAll { SoftwareVersion softwareVersion ->
- !softwareVersion.isSnapshot()
- } as List<SoftwareVersion>).sort { a, b -> b <=> a }
- }
-
- @CompileDynamic
- static List<SoftwareVersion> preReleaseVersions(File releases) {
- versions(releases).findAll { SoftwareVersion softwareVersion ->
- Snapshot snapshot = softwareVersion.snapshot
- snapshot?.isMilestone() || snapshot?.isReleaseCandidate()
- }.sort { a, b -> b <=> a }
- }
-
- static SoftwareVersion latestPreReleaseVersion(File releases) {
- List<SoftwareVersion> versions = preReleaseVersions(releases)
- versions ? versions.get(0) : null
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/documentation/Snapshot.groovy b/buildSrc/src/main/groovy/org/grails/documentation/Snapshot.groovy
deleted file mode 100644
index 4a4a8e9f3ea..00000000000
--- a/buildSrc/src/main/groovy/org/grails/documentation/Snapshot.groovy
+++ /dev/null
@@ -1,63 +0,0 @@
-package org.grails.documentation
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class Snapshot implements Comparable<Snapshot> {
-
- private String text
-
- int getMilestoneVersion() {
- text.replace("M", "").toInteger()
- }
-
- int getReleaseCandidateVersion() {
- text.replace("RC", "").toInteger()
- }
-
- boolean isBuildSnapshot() {
- text.endsWith("SNAPSHOT")
- }
-
- boolean isReleaseCandidate() {
- text.startsWith("RC")
- }
-
- boolean isMilestone() {
- text.startsWith("M")
- }
-
- Snapshot(String text) {
- this.text = text
- }
-
- @Override
- int compareTo(Snapshot o) {
-
- if (this.buildSnapshot && !o.buildSnapshot) {
- return 1
- } else if (!this.buildSnapshot && o.buildSnapshot) {
- return -1
- } else if (this.buildSnapshot && o.buildSnapshot) {
- return 0
- }
-
- if (this.releaseCandidate && !o.releaseCandidate) {
- return 1
- } else if (!this.releaseCandidate && o.releaseCandidate) {
- return -1
- } else if (this.releaseCandidate && o.releaseCandidate) {
- return this.releaseCandidateVersion <=> o.releaseCandidateVersion
- }
-
- if (this.milestone && !o.milestone) {
- return 1
- } else if (!this.milestone && o.milestone) {
- return -1
- } else if (this.milestone && o.milestone) {
- return this.milestoneVersion <=> o.milestoneVersion
- }
-
- return 0
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/events/Event.groovy b/buildSrc/src/main/groovy/org/grails/events/Event.groovy
deleted file mode 100644
index 1a27e6684a1..00000000000
--- a/buildSrc/src/main/groovy/org/grails/events/Event.groovy
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.grails.events
-
-import edu.umd.cs.findbugs.annotations.Nullable
-import groovy.transform.CompileStatic
-import groovy.transform.ToString
-
-import java.time.LocalDate
-
-@ToString
-@CompileStatic
-class Event {
- String name
- String location
- LocalDate date
- List<String> speakers
-
- @Nullable
- String link
-}
diff --git a/buildSrc/src/main/groovy/org/grails/events/EventsPage.groovy b/buildSrc/src/main/groovy/org/grails/events/EventsPage.groovy
deleted file mode 100644
index c4f255ba444..00000000000
--- a/buildSrc/src/main/groovy/org/grails/events/EventsPage.groovy
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.grails.events
-
-import groovy.transform.CompileDynamic
-
-@CompileDynamic
-class EventsPage {
-
- @CompileDynamic
- static String eventsTable(List<Event> events, boolean showLocation = false) {
- String msg = """\
-<div class="training"><table>
-<colgroup>
-<col>
-<col>
-${showLocation ? '<col>': ''}
-<col>
-</colgroup>
-<thead>
-<tr><th>Event</th><th>Date(s)</th>${showLocation ? '<th>Location</th>': ''}<th>Speakers</th></tr>
-</thead>
-<tbody>
-"""
- for (Event event : events) {
- msg += tableRowForEvent(event, showLocation)
- }
- msg += '</tbody>'
- msg += '</table></div>'
- msg
- }
-
- static String tableRowForEvent(Event event, boolean showLocation) {
- String msg = '<tr>'
- if (event.link) {
- msg += "<td><a href=\"${event.link}\">${event.name ?: ''}</a></td>"
- } else {
- msg += "<td>${event.name ?: ''}</td>"
- }
- msg += "<td>${event.date ?: ''}</td>"
- if (showLocation) {
- msg += "<td>${event.location ?: ''}</td>"
- }
- msg += "<td>${(event.speakers ? event.speakers.join(', ') : '')}</td>"
- msg += "</tr>"
- msg
- }
-
-}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/BlogTask.groovy b/buildSrc/src/main/groovy/org/grails/gradle/tasks/BlogTask.groovy
deleted file mode 100644
index e4c802e6aba..00000000000
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/BlogTask.groovy
+++ /dev/null
@@ -1,701 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.grails.gradle.tasks
-
-import java.time.Instant
-import java.time.LocalDateTime
-import java.time.ZoneId
-import java.time.ZonedDateTime
-import java.util.stream.Collectors
-
-import javax.annotation.Nonnull
-import javax.inject.Inject
-
-import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import groovy.xml.MarkupBuilder
-
-import io.micronaut.rss.DefaultRssFeedRenderer
-import io.micronaut.rss.RssChannel
-import io.micronaut.rss.RssItem
-import org.gradle.api.Project
-import org.gradle.api.file.CopySpec
-import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.model.ObjectFactory
-import org.gradle.api.provider.ListProperty
-import org.gradle.api.provider.Property
-import org.gradle.api.tasks.CacheableTask
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputDirectory
-import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.OutputDirectory
-import org.gradle.api.tasks.PathSensitive
-import org.gradle.api.tasks.PathSensitivity
-import org.gradle.api.tasks.TaskAction
-import org.gradle.api.tasks.TaskProvider
-
-import org.grails.utils.DateUtils
-import org.grails.HtmlPost
-import org.grails.MarkdownPost
-import org.grails.PostMetadata
-import org.grails.PostMetadataAdapter
-import org.grails.documentation.SiteMap
-import org.grails.gradle.GrailsWebsiteExtension
-import org.grails.utils.MarkdownUtils
-import org.grails.tags.Tag
-import org.grails.tags.TagCloud
-
-@CompileStatic
-@CacheableTask
-class BlogTask extends GrailsWebsiteTask {
-
- @Internal
- String description =
- 'Renders Markdown posts (posts/*.md) into HTML pages (dist/blog/*.html). ' +
- 'It generates tag pages. Generates RSS feed. ' +
- 'Posts with future dates are not generated.'
-
- public static final String NAME = 'renderBlog'
-
- private static final String RSS_FILE = 'rss.xml'
- private static final String IMAGES = 'images'
- private static final String HASHTAG_SPAN = '<span class="hashtag">#'
- private static final String SPAN_CLOSE = "</span>"
- private static final int MAX_RELATED_POSTS = 3
- private static final String BLOG = 'blog'
- private static final String TAG = 'tag'
- private static final String INDEX = 'index.html'
- private static List<String> ALLOWED_TAG_PREFIXES = new ArrayList<>()
- private static final int MAX_TITLE_LENGTH = 45
-
- static {
- def characters = 'A'..'Z'
- def digits = 0..9
- def l = characters.stream()
- .map({str -> "#${str}".toString()})
- .collect(Collectors.toList())
- l.addAll(characters.stream()
- .map({str -> "#${str.toLowerCase()}".toString()})
- .collect(Collectors.toList()))
- l.addAll(digits.stream()
- .map({digit -> "#${digit}".toString()})
- .collect(Collectors.toList()))
- ALLOWED_TAG_PREFIXES = l
- }
-
- private final ObjectFactory objects
-
- @Inject
- BlogTask(ObjectFactory objects) {
- this.objects = objects
- }
-
- @InputFile
- @PathSensitive(PathSensitivity.RELATIVE)
- final RegularFileProperty document = objects.fileProperty()
-
- @InputFile
- @PathSensitive(PathSensitivity.RELATIVE)
- final RegularFileProperty releases = objects.fileProperty()
-
- @InputDirectory
- @PathSensitive(PathSensitivity.RELATIVE)
- final DirectoryProperty assetsDir = objects.directoryProperty()
-
- @InputDirectory
- @PathSensitive(PathSensitivity.RELATIVE)
- final DirectoryProperty postsDir = objects.directoryProperty()
-
- @Input
- final Property<String> about = objects.property(String)
-
- @Input
- final ListProperty<String> keywords = objects.listProperty(String)
-
- @Input
- final Property<String> robots = objects.property(String)
-
- @Input
- final Property<String> title = objects.property(String)
-
- @Input
- final Property<String> url = objects.property(String)
-
- @OutputDirectory
- final DirectoryProperty outputDir = objects.directoryProperty()
-
- static TaskProvider<BlogTask> register(
- Project project,
- GrailsWebsiteExtension siteExt,
- String name = NAME
- ) {
- project.tasks.register(name, BlogTask) {
- it.about.set(siteExt.description)
- it.assetsDir.set(siteExt.assetsDir)
- it.document.set(siteExt.template)
- it.keywords.set(siteExt.keywords)
- it.outputDir.set(siteExt.outputDir)
- it.postsDir.set(siteExt.postsDir)
- it.releases.set(siteExt.releases)
- it.robots.set(siteExt.robots)
- it.title.set(siteExt.title)
- it.url.set(siteExt.url)
- }
- }
-
- @TaskAction
- void renderBlog() {
- def meta = RenderSiteTask.siteMeta(
- title.get(),
- about.get(),
- url.get(),
- keywords.get(),
- robots.get(),
- SiteMap.latestVersion(releases.get().asFile).versionText,
- SiteMap.olderVersions(releases.get().asFile)
- .reverse()
- .collect {"<option>$it</option>" }
- .join(' ')
- )
- renderPosts(
- meta,
- processPosts(
- meta,
- parsePosts(postsDir.get().asFile)
- .findAll {
- !it.parsedDate.after(new Date())
- }
- .sort(false)
- ),
- outputDir.dir('dist/blog').get().asFile.tap { it.mkdirs() },
- document.get().asFile.text
- )
- copyBackgroundImages()
- copyBlogImages()
- }
-
- void copyBlogImages() {
- project.copy { CopySpec copy ->
- copy.from(postsDir)
- copy.into(outputDir.dir('dist/blog'))
- copy.include(CopyAssetsTask.IMAGE_EXTENSIONS)
- }
- }
-
- void copyBackgroundImages() {
- project.copy { CopySpec copy ->
- copy.from(assetsDir.dir('bgimages'))
- copy.into(outputDir.dir('dist/images'))
- copy.include(CopyAssetsTask.IMAGE_EXTENSIONS)
- }
- }
-
- static RssItem rssItemWithPage(
- String title,
- Date pubDate,
- String link,
- String guid,
- String html,
- String author = null
- ) {
- def htmlWithoutTitleAndDate = html
- if (htmlWithoutTitleAndDate.contains('<span class="date">')) {
- htmlWithoutTitleAndDate = htmlWithoutTitleAndDate
- .substring(htmlWithoutTitleAndDate.indexOf('<span class="date">'))
- htmlWithoutTitleAndDate = htmlWithoutTitleAndDate
- .substring(htmlWithoutTitleAndDate.indexOf('</span>') + '</span>'.length())
- }
- def builder = RssItem.builder()
- .title(title)
- .pubDate(
- ZonedDateTime.of(
- Instant.ofEpochMilli(pubDate.time)
- .atZone(ZoneId.systemDefault())
- .toLocalDateTime(), ZoneId.of('GMT')
- )
- )
- .link(link)
- .guid(guid)
- .description(htmlWithoutTitleAndDate)
- if (author) {
- builder = builder.author(parseAuthorName(author))
- }
- builder.build()
- }
-
- static String parseAuthorName(String author) {
- author.contains(' (') ? author.substring(0, author.indexOf(' (')) : author
- }
-
- @CompileDynamic
- static String renderPostHtml(
- HtmlPost htmlPost,
- String templateText,
- List<HtmlPost> posts
- ) {
- def writer = new StringWriter()
- def mb = new MarkupBuilder(writer)
- mb.div(class: 'headerbar chalicesbg') {
- div(class: 'content') {
- h1 {
- a(href: '[%url]/blog/index.html','Grails Blog')
- }
- }
- }
- mb.div(class: 'content container') {
- div(class: 'light padded blogpost') {
- mkp.yieldUnescaped(htmlPost.html)
- h2(class: 'space-above') {
- span('You might also like ...')
- }
- div(class: 'threecolumns') {
- for (HtmlPost post : relatedPosts(htmlPost, posts)) {
- div(class: 'column') {
- mkp.yieldUnescaped(postCard(post))
- }
- }
- }
- }
- }
- String html = writer.toString()
- Map<String, String> metadata = htmlPost.metadata.toMap()
- html = RenderSiteTask.renderHtmlWithTemplateContent(html, metadata, templateText)
- html = RenderSiteTask.highlightMenu(html, metadata, htmlPost.path)
- metadata['body'] = metadata['body'] ? metadata['body'] : ''
- if (metadata['body']) {
- html = html.replace("<body>", "<body class='${metadata['body']}'>")
- }
- html
- }
-
- static List<HtmlPost> relatedPosts(HtmlPost htmlPost, List<HtmlPost> posts) {
- List<HtmlPost> relatedPosts = []
- for (String tag : htmlPost.tags) {
- for (HtmlPost p : posts) {
- if (p.tags.contains(tag) && p.path != htmlPost.path) {
- List<String> paths = relatedPosts*.path
- if (paths.contains(p.path)) {
- continue
- }
- relatedPosts.add(p)
- if (relatedPosts.size() > MAX_RELATED_POSTS) {
- break
- }
- }
- }
- if (relatedPosts.size() > MAX_RELATED_POSTS) {
- break
- }
- }
- if (relatedPosts.size() < MAX_RELATED_POSTS) {
- for (def p : posts) {
- def paths = relatedPosts*.path
- paths.add(htmlPost.path)
- if (paths.contains(p.path)) {
- continue
- }
- relatedPosts.add(p)
- if (relatedPosts.size() > MAX_RELATED_POSTS) {
- break
- }
- }
- }
- relatedPosts.subList(0, MAX_RELATED_POSTS).sort { a, b ->
- DateUtils.parseDate(a.metadata.date).after(DateUtils.parseDate(b.metadata.date)) ? -1 : 1
- }
- }
-
- static List<HtmlPost> processPosts(Map<String, String> globalMetadata, List<MarkdownPost> markdownPosts) {
- markdownPosts.collect { MarkdownPost mdPost ->
- Map<String, String> metadata = RenderSiteTask.processMetadata(globalMetadata + mdPost.metadata)
- PostMetadata postMetadata = new PostMetadataAdapter(metadata)
- String markdown = mdPost.content
- if (metadata.containsKey('slides')) {
- markdown = markdown + "\n\n[Slides](${metadata['slides']})\n\n"
- }
- if (metadata.containsKey('code')) {
- markdown = markdown + "\n\n[Code](${metadata['code']})\n\n"
- }
- String html = MarkdownUtils.htmlFromMarkdown(markdown)
- String iframe = RenderSiteTask.parseVideoIframe(metadata)
- if (iframe) {
- html = html + iframe
- }
- String contentHtml = wrapTags(metadata, html)
- Set<String> postTags = parseTags(contentHtml)
- new HtmlPost(metadata: postMetadata, html: contentHtml, path: mdPost.path, tags: postTags)
- }
- }
-
- static void renderPosts(
- Map<String, String> globalMetadata,
- List<HtmlPost> listOfPosts,
- File outputDir,
- final String templateText
- ) {
- List<String> postCards = []
- List<RssItem> rssItems = []
- Map<String, List<String>> tagPosts = [:]
- Map<String, Integer> tagsMap = [:]
-
- for (HtmlPost htmlPost : listOfPosts) {
- postCards << postCard(htmlPost)
- def html = renderPostHtml(htmlPost, templateText, listOfPosts)
- new File(outputDir, htmlPost.path).tap {
- it.createNewFile()
- it.text = html
- }
-
- def postTags = parseTags(html)
- for (String postTag : postTags) {
- tagsMap[postTag] = tagsMap.containsKey(postTag) ? (1 + tagsMap[postTag]) : 1
- if (!tagPosts.containsKey(postTag)) {
- tagPosts[postTag] = []
- }
- tagPosts[postTag] << htmlPost.path
- }
- def postLink = postLink(htmlPost)
- rssItems.add(
- rssItemWithPage(
- htmlPost.metadata.title,
- DateUtils.parseDate(htmlPost.metadata.date),
- postLink,
- htmlPost.path.replace('.html', ''),
- htmlPost.html,
- htmlPost.metadata.author
- )
- )
- }
- def tags = tagsMap.collect {k, v ->
- new Tag(title: k, ocurrence: v)
- } as Set<Tag>
- renderArchive(new File(outputDir, 'index.html'), postCards, globalMetadata, templateText, tags)
- renderRss(globalMetadata, rssItems, new File(outputDir.parentFile, RSS_FILE))
- renderTags(globalMetadata, outputDir, tagsMap.keySet(), listOfPosts, templateText)
- }
-
- static Set<String> parseTags(String html) {
- def pageHtml = html
- def tags = [] as Set<String>
- for (; ;) {
- if (!(pageHtml.contains(HASHTAG_SPAN) && pageHtml.contains(SPAN_CLOSE))) {
- return tags
- }
- pageHtml = pageHtml.substring(pageHtml.indexOf(HASHTAG_SPAN) + HASHTAG_SPAN.length())
- def tag = pageHtml.substring(0, pageHtml.indexOf(SPAN_CLOSE))
- tags << tag
- pageHtml = pageHtml.substring(pageHtml.indexOf(SPAN_CLOSE) + SPAN_CLOSE.length())
- }
- }
-
- static void renderTags(
- Map<String, String> metadata,
- File outputDir,
- Set<String> tags,
- List<HtmlPost> posts,
- String templateText
- ) {
- def tagFolder = new File(outputDir, TAG).tap {
- it.mkdirs()
- }
-
- def resolvedMetadata = RenderSiteTask.processMetadata(metadata)
-
- for (String tag : tags) {
- def tagCards = [] as List<String>
- def postsTagged = posts.findAll { it.tags.contains(tag) }
- for (def post : postsTagged) {
- tagCards << postCard(post)
- }
- def tagFile = new File(tagFolder, "${tag}.html")
- resolvedMetadata['title'] = "${tag.toUpperCase()} | Blog | Grails Framework".toString()
- renderCards(tagFile, tagCards, resolvedMetadata, templateText, renderTagTitle(tag))
- }
- }
-
- static String postLink(HtmlPost post) {
- "$post.metadata.url/$BLOG/$post.path"
- }
-
- @CompileDynamic
- private static String postCard(HtmlPost htmlPost) {
- def imageUrl = htmlPost.metadata['image'] ?
- "$htmlPost.metadata.url/$IMAGES/${htmlPost.metadata['image']}".toString() :
- null
- def writer = new StringWriter()
- def mb = new MarkupBuilder(writer)
- mb.article(class: 'blogcard', style: imageUrl ? 'background-image: url(' + imageUrl + ')' : '') {
- a(href: postLink(htmlPost)) {
- h3 {
- mkp.yield(
- RenderSiteTask.formatDate(htmlPost.metadata.date)
- )
- }
- h2 {
- def title = RenderSiteTask.replaceLineWithMetadata(
- htmlPost.metadata.title,
- htmlPost.metadata.toMap()
- )
- if (title.length() > MAX_TITLE_LENGTH) {
- title = "${title.substring(0, MAX_TITLE_LENGTH)}..."
- }
- mkp.yield(title)
- }
- }
- }
- writer.toString()
- }
-
- @CompileDynamic
- private static String renderTagTitle(String tag) {
- def writer = new StringWriter()
- def mb = new MarkupBuilder(writer)
- mb.h1 {
- span('Tag:')
- b(tag)
- }
- writer.toString()
- }
-
- @CompileDynamic
- private static String tagsCard(Map<String, String> sitemeta, Set<Tag> tags) {
- def writer = new StringWriter()
- def mb = new MarkupBuilder(writer)
- mb.article(class: 'tagcloud blogcard desktop') {
- h3('Post by Tag')
- mkp.yieldUnescaped(
- TagCloud.tagCloud(
- "${sitemeta['url']}/${BLOG}/${TAG}",
- tags,
- false
- )
- )
- }
- writer.toString()
- }
-
- @CompileDynamic
- private static String rssCard(String url) {
- def writer = new StringWriter()
- def mb = new MarkupBuilder(writer)
- def imageUrl = "$url/images/feedicon.svg"
- mb.article(
- class: 'blogcard desktop',
- style: /background-image: url('/ + imageUrl + /')/
- ) {
- h3('Feeds')
- h2 {
- a(href: '[%url]/' + RSS_FILE, 'RSS Feed')
- }
- }
- writer.toString()
- }
-
- @CompileDynamic
- private static String subscribeCard() {
- def writer = new StringWriter()
- def mb = new MarkupBuilder(writer)
- mb.article(class: 'blogcard desktop') {
- mkp.yieldUnescaped('''
-<!--[if lte IE 8]>
-<script charset="utf-8" type="text/javascript" src="//js.hsforms.net/forms/v2-legacy.js"></script>
-<![endif]-->
-<script charset="utf-8" type="text/javascript" src="//js.hsforms.net/forms/v2.js"></script>
-<script>
- hbspt.forms.create({
-\tportalId: "4547412",
-\tformId: "a675210e-7748-44bf-b603-3363d613ffb1"
-});
-</script>
-''')
- }
- writer.toString()
- }
-
- @CompileDynamic
- private static void renderArchive(
- File f,
- List<String> postCards,
- Map<String, String> sitemeta,
- String templateText,
- Set<Tag> tags
- ) {
- def cards = [] as List<String>
- cards.addAll(postCards)
-// cards.add(2, tagsCard(sitemeta, tags))
-// cards.add(5, rssCard(sitemeta['url']))
- //cards.add(8, subscribeCard())
- def resolvedMetadata = RenderSiteTask.processMetadata(sitemeta)
- // String html = EventsPage.mainContent(sitemeta['url']) +
- // cardsHtml(cards, resolvedMetadata)
- def html = cardsHtml(cards, resolvedMetadata)
- resolvedMetadata['title'] = 'Blog | Grails Framework'
- html = RenderSiteTask.renderHtmlWithTemplateContent(html, resolvedMetadata, templateText)
- html = RenderSiteTask.highlightMenu(html, resolvedMetadata, "/$BLOG/$INDEX")
- f.createNewFile()
- f.text = html
- }
-
- private static void renderCards(
- File f,
- List<String> cards,
- Map<String, String> meta,
- String templateText,
- String title = null
- ) {
- def pageHtml = cardsHtml(cards, meta, title)
- f.createNewFile()
- f.text = RenderSiteTask.renderHtmlWithTemplateContent(pageHtml, meta, templateText)
- }
-
- @CompileDynamic
- static String cardsHtml(
- List<String> cards,
- Map<String, String> meta,
- String title = null
- ) {
- def writer = new StringWriter()
- def mb = new MarkupBuilder(writer)
- mb.div(class: 'headerbar chalicesbg') {
- div(class: 'content') {
- if (title) {
- mkp.yieldUnescaped(title)
- } else {
- h1 {
- a(href: '[%url]/blog/index.html', 'Grails Blog')
- }
- }
- }
- }
- mb.div(class: 'clear content container') {
-// if (title) {
-// mkp.yieldUnescaped(title)
-// } else {
-//
-// }
-
- div(class: 'light') {
- div(class: 'padded', style: 'padding-top: 0;') {
- for (int i = 0; i < cards.size(); i++) {
- if (i == 0) {
- mkp.yieldUnescaped('<div class="threecolumns">')
- }
- div(class: 'column') {
- mkp.yieldUnescaped(cards[i])
- }
- if (i != 0 && ((i + 1 ) % 3 == 0)) {
- mkp.yieldUnescaped('</div>')
- if (i != (cards.size() - 1)) {
- mkp.yieldUnescaped('<div class="threecolumns">')
- }
- }
- }
- }
- }
- }
- writer.toString()
- }
-
- private static void renderRss(
- Map<String, String> sitemeta,
- List<RssItem> rssItems,
- File outputFile
- ) {
- def builder = RssChannel.builder(
- sitemeta['title'],
- sitemeta['url'],
- sitemeta['description']
- )
- builder.pubDate(
- ZonedDateTime.of(
- LocalDateTime.now(),
- ZoneId.of('GMT')
- )
- )
- builder.lastBuildDate(
- ZonedDateTime.of(
- LocalDateTime.now(),
- ZoneId.of('GMT')
- )
- )
- .docs('https://blogs.law.harvard.edu/tech/rss')
- .generator('Micronaut RSS')
- .managingEditor('delamos@objectcomputing.com')
- .webMaster('delamos@objectcomputing.com')
-
- for (RssItem item : rssItems) {
- builder.item(item)
- }
- def writer = new FileWriter(outputFile)
- new DefaultRssFeedRenderer().with {
- render(writer, builder.build())
- }
- writer.close()
- }
-
- static boolean isTag(String word) {
- ALLOWED_TAG_PREFIXES.any {word.startsWith(it) }
- }
-
- @Nonnull
- static String wrapTags(Map<String, String> metadata, @Nonnull String html) {
- html.split('\n')
- .collect { line ->
- if (line.startsWith('<p>') && line.endsWith('</p>')) {
- def lineWithoutParagraphs = line
- .replaceAll('<p>', '')
- .replaceAll('</p>', '')
- def words = lineWithoutParagraphs.split(' ')
- lineWithoutParagraphs = words.collect { word ->
- if (isTag(word)) {
- def tag = word
- if (word.contains('<')) {
- tag = word.substring(0, word.indexOf('<'))
- }
- return "<a href=\"${metadata['url']}/$BLOG/$TAG/${tag.replaceAll('#', '')}.html\"><span class=\"hashtag\">$tag</span></a>".toString()
- } else {
- return word
- }
- }.join(' ')
- return "<p>$lineWithoutParagraphs</p>".toString()
- } else {
- return line
- }
- }.join('\n')
- }
-
- static List<MarkdownPost> parsePosts(File postsDir) {
- List<MarkdownPost> listOfPosts = []
- postsDir.eachFile { file ->
- if (file.path.endsWith('.md') || file.path.endsWith('.markdown')) {
- def contentAndMetadata = RenderSiteTask.parseFile(file)
- listOfPosts.add(
- new MarkdownPost(
- filename: file.name,
- content: contentAndMetadata.content,
- metadata: contentAndMetadata.metadata
- )
- )
- }
- }
- listOfPosts
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/CopyAssetsTask.groovy b/buildSrc/src/main/groovy/org/grails/gradle/tasks/CopyAssetsTask.groovy
deleted file mode 100644
index acddefc2c9a..00000000000
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/CopyAssetsTask.groovy
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.grails.gradle.tasks
-
-import javax.inject.Inject
-
-import groovy.transform.CompileStatic
-
-import org.gradle.api.Project
-import org.gradle.api.file.CopySpec
-import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.model.ObjectFactory
-import org.gradle.api.tasks.CacheableTask
-import org.gradle.api.tasks.InputDirectory
-import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.OutputDirectory
-import org.gradle.api.tasks.PathSensitive
-import org.gradle.api.tasks.PathSensitivity
-import org.gradle.api.tasks.TaskAction
-import org.gradle.api.tasks.TaskProvider
-
-import org.grails.gradle.GrailsWebsiteExtension
-
-@CompileStatic
-@CacheableTask
-class CopyAssetsTask extends GrailsWebsiteTask {
-
- @Internal
- String description = 'Copies css, js, fonts and images from the assets folder to the dist folder'
-
- public static final String NAME = 'copyAssets'
-
- public static final List<String> FONT_EXTENSIONS = ['*.eot', '*.ttf', '*.woff', '*.woff2']
- public static final List<String> JAVASCRIPT_EXTENSIONS = ['*.js']
- public static final List<String> CSS_EXTENSIONS = ['*.css']
- public static final List<String> IMAGE_EXTENSIONS = ['*.ico', '*.png', '*.svg', '*.jpg', '*.jpeg', '*.gif']
- public static final List<String> FILE_EXTENSIONS = ['*.jar', '*.md5', '*.pdf']
-
- private final ObjectFactory objects
-
- @Inject
- CopyAssetsTask(ObjectFactory objects) {
- this.objects = objects
- }
-
- @InputDirectory
- @PathSensitive(PathSensitivity.RELATIVE)
- final DirectoryProperty assetsDir = objects.directoryProperty()
-
- @OutputDirectory
- final DirectoryProperty outputDir = objects.directoryProperty()
-
- @TaskAction
- void copyAssets() {
- copyImages()
- copyCss()
- copyJs()
- copyFonts()
- copyFiles()
- }
-
- static TaskProvider<CopyAssetsTask> register(Project project, GrailsWebsiteExtension siteExt) {
- project.tasks.register(NAME, CopyAssetsTask) {
- it.outputDir.set(siteExt.outputDir)
- it.assetsDir.set(siteExt.assetsDir)
- }
- }
-
- private File getDistDir() {
- new File(outputDir.get().asFile, 'dist').tap { it.mkdirs() }
- }
-
- static List<String> recursiveIncludes(Iterable<String> extensions) {
- extensions.collect {
- ["$it", "**/$it"]
- }.flatten() as List<String>
- }
-
- void copyImages() {
- def srcDir = new File(assetsDir.get().asFile, 'images')
- def destDir = new File(distDir, 'images').tap { it.mkdirs() }
- project.copy { CopySpec copySpec ->
- copySpec.from(srcDir)
- copySpec.into(destDir)
- copySpec.include(recursiveIncludes(IMAGE_EXTENSIONS))
- copySpec.setIncludeEmptyDirs(false)
- }
- }
-
- void copyCss() {
- def cssSrcDir = new File(assetsDir.get().asFile, 'stylesheets')
- def cssDstDir = new File(distDir, 'stylesheets').tap {it.mkdirs() }
- project.copy { CopySpec copySpec ->
- copySpec.from(cssSrcDir)
- copySpec.into(cssDstDir)
- copySpec.include(CSS_EXTENSIONS)
- }
- }
-
- void copyFonts() {
- def dstFontsDir = new File(distDir, 'fonts').tap { it.mkdirs() }
- def srcFontsDir = new File(assetsDir.get().asFile, 'fonts')
- project.copy { CopySpec copySpec ->
- copySpec.from(srcFontsDir)
- copySpec.into(dstFontsDir)
- copySpec.include(FONT_EXTENSIONS)
- }
- }
-
- void copyJs() {
- def jsDstDir = new File(distDir, 'javascripts').tap { it.mkdirs() }
- def jsSrcDir = new File(assetsDir.get().asFile, 'javascripts')
- project.copy { CopySpec copySpec ->
- copySpec.from(jsSrcDir)
- copySpec.into(jsDstDir)
- copySpec.include(JAVASCRIPT_EXTENSIONS)
- }
- }
-
- void copyFiles() {
- def filesDstDir = new File(distDir, 'files').tap { it.mkdirs() }
- def filesSrcDir = new File(assetsDir.get().asFile, 'files')
- project.copy { CopySpec copySpec ->
- copySpec.from(filesSrcDir)
- copySpec.into(filesDstDir)
- copySpec.include(recursiveIncludes(FILE_EXTENSIONS))
- }
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/PluginsTask.groovy b/buildSrc/src/main/groovy/org/grails/gradle/tasks/PluginsTask.groovy
deleted file mode 100644
index 2b2944e7bb7..00000000000
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/PluginsTask.groovy
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.grails.gradle.tasks
-
-import java.net.http.HttpClient
-import java.net.http.HttpRequest
-import java.net.http.HttpResponse
-import java.time.LocalDateTime
-import java.time.format.DateTimeFormatter
-import java.util.stream.Collectors
-
-import javax.inject.Inject
-
-import groovy.json.JsonSlurper
-import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import groovy.util.logging.Slf4j
-
-import org.gradle.api.Project
-import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.model.ObjectFactory
-import org.gradle.api.provider.ListProperty
-import org.gradle.api.provider.Property
-import org.gradle.api.tasks.CacheableTask
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.InputFile
-import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.OutputDirectory
-import org.gradle.api.tasks.PathSensitive
-import org.gradle.api.tasks.PathSensitivity
-import org.gradle.api.tasks.TaskAction
-import org.gradle.api.tasks.TaskProvider
-
-import org.grails.gradle.GrailsWebsiteExtension
-import org.grails.plugin.Owner
-import org.grails.plugin.Plugin
-import org.grails.plugin.PluginsPage
-
-@Slf4j
-@CompileStatic
-@CacheableTask
-class PluginsTask extends GrailsWebsiteTask {
-
- @Internal
- String description = 'Generates an HTML Page listing the Grails plugins'
-
- public static final String NAME = 'genPlugins'
-
- private static final String GRAILS_PLUGINS_JSON =
- 'https://raw.githubusercontent.com/grails/grails-plugins-metadata/main/grails-plugins.json'
-
- private final ObjectFactory objects
-
- @Inject
- PluginsTask(ObjectFactory objects) {
- this.objects = objects
- }
-
- @InputFile
- @PathSensitive(PathSensitivity.RELATIVE)
- final RegularFileProperty document = objects.fileProperty()
-
- @Input
- final ListProperty<String> keywords = objects.listProperty(String)
-
- @Input
- final Property<String> url = objects.property(String)
-
- @OutputDirectory
- final DirectoryProperty outputDir = objects.directoryProperty()
-
- static TaskProvider<PluginsTask> register(
- Project project,
- GrailsWebsiteExtension siteExt,
- String name = NAME
- ) {
- project.tasks.register(name, PluginsTask) {
- it.document.set(siteExt.template)
- it.outputDir.set(siteExt.outputDir)
- it.url.set(siteExt.url)
- }
- }
-
- @TaskAction
- void renderPluginsPage() {
- def template = document.get().asFile
- def templateText = template.text
- def metadata = RenderSiteTask.siteMeta(
- 'Grails Plugins', // TODO Make it configurable
- 'List of Plugins', // TODO Make it configurable
- url.get(),
- keywords.get(),
- 'all', // TODO Make it configurable,
- '',
- ''
- )
- metadata.put('JAVASCRIPT', '/javascripts/plugins-search.js')
-
- def resolvedMetadata = RenderSiteTask.processMetadata(metadata)
- def result = new JsonSlurper().parse(GRAILS_PLUGINS_JSON.toURL())
- def plugins = pluginsFromJson(result)
- renderHtml(plugins, templateText, resolvedMetadata, 'plugins.html')
- }
-
- void renderHtml(List<Plugin> plugins, String templateText, Map<String, String> metadata, String fileName) {
- def siteUrl = url.get()
- def distDir = new File(outputDir.get().asFile, 'dist').tap { it.mkdir() }
- def pluginsDir = new File(distDir, 'plugins').tap { it.mkdir() }
- def pluginsTagsDir = new File(pluginsDir, 'tags').tap { it.mkdir() }
- def pluginsOwnersDir = new File(pluginsDir, 'owners').tap { it.mkdir() }
- def pluginOutputFile = new File(distDir, fileName).tap { it.createNewFile() }
- def html = PluginsPage.mainContent(siteUrl, plugins, 'Grails Plugins', null)
- html = RenderSiteTask.renderHtmlWithTemplateContent(html, metadata, templateText)
- html = RenderSiteTask.highlightMenu(html, metadata, '/plugins.html')
- pluginOutputFile.text = html
-
- def tags = plugins.stream().flatMap(p -> p.labels.stream()).distinct().collect(Collectors.toList())
- for (def tag in tags) {
- def tagsOutputFile = new File(pluginsTagsDir, "${tag}.html").tap { it.createNewFile() }
- def htmlForTags = renderHtmlPagesForTags(siteUrl, plugins, tag)
- tagsOutputFile.text = RenderSiteTask.renderHtmlWithTemplateContent(htmlForTags, metadata, templateText)
- }
-
- def owners = plugins.stream().map(p -> p.owner.name).distinct().collect(Collectors.toList())
- for (def owner in owners) {
- def ownersOutputFile = new File(pluginsOwnersDir, "${owner}.html").tap { it.createNewFile() }
- def htmlForOwnersFile = renderHtmlPagesForOwners(siteUrl, plugins, owner)
- ownersOutputFile.text = RenderSiteTask.renderHtmlWithTemplateContent(htmlForOwnersFile, metadata, templateText)
- }
- }
-
- static String renderHtmlPagesForTags(String siteUrl, List<Plugin> plugins, String tag) {
- def filteredPlugins = plugins.stream().filter(p -> p.labels.contains(tag)).collect(Collectors.toList())
- PluginsPage.mainContent(siteUrl, plugins , "Plugins by tag #${tag}", filteredPlugins)
- }
-
- static String renderHtmlPagesForOwners(String siteUrl, List<Plugin> plugins, String owner) {
- def filteredPlugins = plugins.stream().filter(p -> p.owner.name == owner).collect(Collectors.toList())
- PluginsPage.mainContent(siteUrl, plugins , "Plugins by creator: #${owner}", filteredPlugins)
- }
-
- @CompileDynamic
- List<Plugin> pluginsFromJson(Object json) {
- List<Plugin> plugins = []
- for (int i = 0; i < json.size(); i++) {
- def owner = new Owner(name: json[i].bintrayPackage.owner)
- def updatedDate = parseIsoStringToDate(json[i].bintrayPackage.updated)
- plugins.add(
- new Plugin(
- name: json[i].bintrayPackage.name,
- updated: updatedDate,
- owner: owner,
- desc: json[i].bintrayPackage.desc,
- vcsUrl: json[i].bintrayPackage.vcsUrl,
- latestVersion: json[i].bintrayPackage.latestVersion,
- githubStars: githubStars(json[i].bintrayPackage.vcsUrl).orElse(null),
- labels: json[i].bintrayPackage.labels as List<String>
- )
- )
- }
- Collections.unmodifiableList(plugins)
- }
-
- static LocalDateTime parseIsoStringToDate(String isoFormattedString) {
- def f = DateTimeFormatter.ofPattern(/yyyy-MM-dd'T'HH:mm:ss.SSSXXX/)
- LocalDateTime.parse(isoFormattedString, f)
- }
-
- @CompileDynamic
- static Optional<Integer> githubStars(String vcsUrl) {
- if (!vcsUrl) {
- return Optional.empty()
- }
- if (!vcsUrl.contains('github.com')) {
- return Optional.empty()
- }
- if (!System.getenv('GH_TOKEN')) {
- return Optional.empty()
- }
- try {
- log.debug('Fetching github stars of {}', vcsUrl)
- def url = 'https://api.github.com/repos/' + vcsUrl.substring(vcsUrl.indexOf('github.com/') + 'github.com/'.length())
- def request = HttpRequest.newBuilder()
- .uri(new URI(url))
- .header('X-GitHub-Api-Version', '2022-11-28')
- .header('Authorization', "Bearer ${System.getenv("GH_TOKEN")}")
- .GET()
- .build()
- def response = HttpClient.newBuilder().build().send(
- request,
- HttpResponse.BodyHandlers.ofString()
- )
- if (response.statusCode() == 200) {
- def json = response.body()
- def stars = (new JsonSlurper().parseText(json).stargazers_count) as Integer
- return (stars == 0 ? Optional.empty() : Optional.of(stars)) as Optional<Integer>
- }
- } catch (Exception e) {
- log.error("Error fetching GitHub stars for $vcsUrl", e)
- return Optional.empty()
- }
- return Optional.empty()
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/guides/Category.groovy b/buildSrc/src/main/groovy/org/grails/guides/Category.groovy
deleted file mode 100644
index fa6caaf89b2..00000000000
--- a/buildSrc/src/main/groovy/org/grails/guides/Category.groovy
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.grails.guides
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class Category {
- String name
- String image
-
- String getSlug() {
- String slug = name.replaceAll(' ', '')
- slug = slug.replaceAll('\\(', '')
- slug = slug.replaceAll('\\+', '')
- slug = slug.replaceAll('\\)', '')
- URLEncoder.encode(slug, 'UTF-8').toLowerCase()
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/guides/GrailsVersionedGuide.groovy b/buildSrc/src/main/groovy/org/grails/guides/GrailsVersionedGuide.groovy
deleted file mode 100644
index cf3a6c83fab..00000000000
--- a/buildSrc/src/main/groovy/org/grails/guides/GrailsVersionedGuide.groovy
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.grails.guides
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class GrailsVersionedGuide implements Guide {
- List<String> authors
- String category
- Map<GrailsMajorVersion, List<String>> grailsMayorVersionTags = [:]
- String name
- String title
- String subtitle
- Date publicationDate
- String versionNumber
- String githubSlug
- String githubBranch
-
- @Override
- List<String> getTags() {
- List<List<String>> tags = grailsMayorVersionTags.collect {
- GrailsMajorVersion k, List<String> v ->
- v
- }
- tags << grailsMayorVersionTags.collect {
- GrailsMajorVersion k, List<String> v ->
- k.toString().toLowerCase()
- } as List<String>
- tags.flatten() as List<String>
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/guides/Guide.groovy b/buildSrc/src/main/groovy/org/grails/guides/Guide.groovy
deleted file mode 100644
index d7f99e24fc9..00000000000
--- a/buildSrc/src/main/groovy/org/grails/guides/Guide.groovy
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.grails.guides
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-interface Guide {
- String getVersionNumber()
- List<String> getAuthors()
- String getCategory()
- String getName()
- String getTitle()
- String getSubtitle()
- List<String> getTags()
- Date getPublicationDate()
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/org/grails/guides/GuidesFetcher.groovy b/buildSrc/src/main/groovy/org/grails/guides/GuidesFetcher.groovy
deleted file mode 100644
index db7aea13cda..00000000000
--- a/buildSrc/src/main/groovy/org/grails/guides/GuidesFetcher.groovy
+++ /dev/null
@@ -1,121 +0,0 @@
-package org.grails.guides
-
-import groovy.json.JsonSlurper
-import groovy.time.TimeCategory
-import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import java.text.DateFormat
-import java.text.SimpleDateFormat
-
-@CompileStatic
-class GuidesFetcher {
-
- public static final String GUIDES_JSON = 'https://raw.githubusercontent.com/grails-guides/grails-guides-template/gh-pages/guides.json'
-
- @CompileDynamic
- static List<Guide> fetchGuides(boolean skipFuture = true) {
- URL url = new URL(GUIDES_JSON)
- def jsonArr = new JsonSlurper().parseText(url.text)
- DateFormat dateFormat = new SimpleDateFormat('dd MMM yyyy',
- Locale.US)
-
- Map<String, Set<String>> githubSlugsAndBranches = [:]
- jsonArr.each { it ->
- if (githubSlugsAndBranches.containsKey(it.githubSlug)) {
- Set<String> arr = githubSlugsAndBranches[it.githubSlug]
- arr << it.githubBranch ?: 'master'
- githubSlugsAndBranches[it.githubSlug] = arr
- } else {
- githubSlugsAndBranches[it.githubSlug] = [it.githubBranch ?: 'master'] as HashSet<String>
- }
- }
- List<Guide> guides = []
- for (String githubSlug : githubSlugsAndBranches.keySet()) {
- if (githubSlugsAndBranches[githubSlug].size() == 1) {
- def guideArr = jsonArr.find {
- it.githubSlug == githubSlug &&
- (!it.githubBranch || githubSlugsAndBranches[githubSlug].any { branch -> branch == it.githubBranch})
- }
- if (guideArr) {
- Guide guide = new SingleGuide(
- versionNumber: guideArr.grailsVersion,
- authors: guideArr.authors as List,
- category: guideArr.category,
- githubSlug: guideArr.githubSlug,
- githubBranch: guideArr.githubBranch,
- name: guideArr.name,
- title: guideArr.title,
- subtitle: guideArr.subtitle,
- tags: guideArr.tags as List
- )
- if (guideArr.publicationDate) {
- guide.publicationDate = dateFormat.parse(guideArr.publicationDate as String)
- }
- guides << guide
- }
- } else if (githubSlugsAndBranches[githubSlug].size() > 1) {
- GrailsVersionedGuide guide
- for (String githubBranch : githubSlugsAndBranches[githubSlug]) {
- def guideArr = jsonArr.find {
- it.githubSlug == githubSlug &&
- it.githubBranch == githubBranch
- }
- if (guideArr) {
- if (!guide) {
- guide = new GrailsVersionedGuide()
- }
- guide.versionNumber = guideArr.grailsVersion
- guide.authors = guideArr.authors as List
- guide.category = guideArr.category
- guide.githubSlug = guideArr.githubSlug
- guide.githubBranch = guideArr.githubBranch
- guide.name = guideArr.name
- guide.title = guideArr.title
- guide.subtitle = guideArr.subtitle
- if (githubBranch == 'grails3') {
- guide.grailsMayorVersionTags[GrailsMajorVersion.GRAILS_3] = guideArr.tags
- } else if (githubBranch == 'grails4' || githubBranch == 'master') {
- guide.grailsMayorVersionTags[GrailsMajorVersion.GRAILS_4] = guideArr.tags
- } else if (githubBranch == 'grails5') {
- guide.grailsMayorVersionTags[GrailsMajorVersion.GRAILS_5] = guideArr.tags
- } else if (githubBranch == 'grails6') {
- guide.grailsMayorVersionTags[GrailsMajorVersion.GRAILS_6] = guideArr.tags
- }
- if (guideArr.publicationDate) {
- guide.publicationDate = dateFormat.parse(guideArr.publicationDate as String)
- }
- }
- }
- if (guide) {
- guides << guide
- }
- }
- }
- if(skipFuture) {
- guides = guides.findAll { it.publicationDate.before(tomorrow()) }
- }
- guides.sort { Guide a, Guide b ->
- b.publicationDate <=> a.publicationDate
- }
- }
-
- static ProgrammingLanguage programmingLanguage(String slug) {
- if(slug.endsWith('groovy')) {
- return ProgrammingLanguage.GROOVY
- } else if(slug.endsWith('kotlin')) {
- return ProgrammingLanguage.KOTLIN
- }
-
- return ProgrammingLanguage.JAVA
- }
-
-
- @CompileDynamic
- static Date tomorrow() {
- Date tomorrow = new Date()
- use(TimeCategory) {
- tomorrow = tomorrow += 1.day
- }
- tomorrow
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/guides/MultiLanguageGuide.groovy b/buildSrc/src/main/groovy/org/grails/guides/MultiLanguageGuide.groovy
deleted file mode 100644
index 852594cc9a3..00000000000
--- a/buildSrc/src/main/groovy/org/grails/guides/MultiLanguageGuide.groovy
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.grails.guides
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class MultiLanguageGuide implements Guide {
- List<String> authors
- String category
- Map<ProgrammingLanguage, String> githubSlugs
- Map<ProgrammingLanguage, List<String>> programmingLanguageTags
- String name
- String title
- String subtitle
- Date publicationDate
- String versionNumber
-
- @Override
- List<String> getTags() {
- List<List<String>> tags = programmingLanguageTags.collect { ProgrammingLanguage k, List<String> v -> v}
- tags << programmingLanguageTags.collect {ProgrammingLanguage k, List<String> v -> k.toString().toLowerCase()} as List<String>
- tags.flatten() as List<String>
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/guides/ProgrammingLanguage.groovy b/buildSrc/src/main/groovy/org/grails/guides/ProgrammingLanguage.groovy
deleted file mode 100644
index 56466e36101..00000000000
--- a/buildSrc/src/main/groovy/org/grails/guides/ProgrammingLanguage.groovy
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.grails.guides
-
-import groovy.transform.CompileStatic
-
-@CompileStatic
-enum ProgrammingLanguage {
- JAVA(1),
- KOTLIN(2),
- GROOVY(3)
-
- final int id
- private ProgrammingLanguage(int id) { this.id = id }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/guides/SingleGuide.groovy b/buildSrc/src/main/groovy/org/grails/guides/SingleGuide.groovy
deleted file mode 100644
index e56c832daa6..00000000000
--- a/buildSrc/src/main/groovy/org/grails/guides/SingleGuide.groovy
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.grails.guides
-
-import groovy.transform.CompileStatic
-import groovy.transform.ToString
-
-@ToString(includes = ['title'])
-@CompileStatic
-class SingleGuide implements Guide {
- String versionNumber
- List<String> authors
- String category
- String githubSlug
- String githubBranch
- String name
- String title
- String subtitle
- List<String> tags
- Date publicationDate
-}
diff --git a/buildSrc/src/main/groovy/org/grails/guides/SingleLanguageGuide.groovy b/buildSrc/src/main/groovy/org/grails/guides/SingleLanguageGuide.groovy
deleted file mode 100644
index 47ff078bc28..00000000000
--- a/buildSrc/src/main/groovy/org/grails/guides/SingleLanguageGuide.groovy
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.grails.guides
-
-import groovy.transform.CompileStatic
-import groovy.transform.ToString
-
-@ToString(includes = ['title'])
-@CompileStatic
-class SingleLanguageGuide implements Guide {
- String versionNumber
- List<String> authors
- String category
- String githubSlug
- String name
- String title
- String subtitle
- List<String> tags
- Date publicationDate
- ProgrammingLanguage programmingLanguage
-}
diff --git a/buildSrc/src/main/groovy/org/grails/guides/TagUtils.groovy b/buildSrc/src/main/groovy/org/grails/guides/TagUtils.groovy
deleted file mode 100644
index 24f3a633076..00000000000
--- a/buildSrc/src/main/groovy/org/grails/guides/TagUtils.groovy
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.grails.guides
-
-import groovy.transform.CompileStatic
-import org.grails.plugin.Plugin
-import org.grails.tags.Tag
-
-@CompileStatic
-class TagUtils {
- static Set<Tag> populateTags(List<Guide> guides) {
- Map<String, Integer> tagsMap = [:]
- if ( guides ) {
- for ( Guide guide : guides ) {
- if (guide.tags) {
- for ( String tag : guide.tags ) {
- String k = tag.trim().toLowerCase()
- tagsMap[k] = tagsMap.containsKey(k) ? ( 1 + tagsMap[k] ) : 1
- }
- }
- }
- }
- populateWithTagsMap(tagsMap)
- }
-
- static Set<Tag> populateTagsByPlugins(List<Plugin> plugins) {
- Map<String, Integer> tagsMap = [:]
- if (plugins) {
-
- for (Plugin plugin : plugins) {
- if (plugin.labels) {
- for (String tag : plugin.labels) {
- String k = tag.trim().toLowerCase()
- tagsMap[k] = tagsMap.containsKey(k) ? (1 + tagsMap[k]) : 1
- }
- }
- }
- }
- populateWithTagsMap(tagsMap)
- }
-
- static Set<Tag> populateTagsByPluginsOwners(List<Plugin> plugins) {
- Map<String, Integer> tagsMap = [:]
- if (plugins) {
- for (Plugin plugin : plugins) {
- if (plugin.owner) {
- String tag = plugin.owner.name
- String k = tag.trim().toLowerCase()
- tagsMap[k] = tagsMap.containsKey(k) ? (1 + tagsMap[k]) : 1
- }
- }
- }
- populateWithTagsMap(tagsMap)
- }
-
- private static Set<Tag> populateWithTagsMap(Map<String, Integer> tagsMap) {
- Set<Tag> tags = []
- tagsMap.each { k, v ->
- tags << new Tag(title: k, ocurrence: v)
- }
- tags
- }
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/org/grails/guides/Training.groovy b/buildSrc/src/main/groovy/org/grails/guides/Training.groovy
deleted file mode 100644
index 429dffb1b99..00000000000
--- a/buildSrc/src/main/groovy/org/grails/guides/Training.groovy
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.grails.guides
-
-import groovy.transform.CompileDynamic
-import groovy.xml.MarkupBuilder
-
-class Training {
- @CompileDynamic
- static String training() {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div(class: 'training') {
- h3(class: 'columnheader') {
- a(href: 'https://objectcomputing.com/training/catalog/grails/', 'Grails Training')
- }
- mkp.yieldUnescaped'[%events]'
- }
- writer.toString()
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/plugin/Owner.groovy b/buildSrc/src/main/groovy/org/grails/plugin/Owner.groovy
deleted file mode 100644
index 0ecfdcf2dee..00000000000
--- a/buildSrc/src/main/groovy/org/grails/plugin/Owner.groovy
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.grails.plugin
-
-import groovy.transform.EqualsAndHashCode
-
-@EqualsAndHashCode
-class Owner {
- String name;
- List<Plugin> plugins;
-}
diff --git a/buildSrc/src/main/groovy/org/grails/plugin/Plugin.groovy b/buildSrc/src/main/groovy/org/grails/plugin/Plugin.groovy
deleted file mode 100644
index dbff42cdc4a..00000000000
--- a/buildSrc/src/main/groovy/org/grails/plugin/Plugin.groovy
+++ /dev/null
@@ -1,34 +0,0 @@
-package org.grails.plugin
-
-import groovy.transform.CompileStatic
-import groovy.transform.EqualsAndHashCode
-import groovy.transform.Sortable
-import org.jetbrains.annotations.NotNull
-
-import java.time.LocalDateTime
-
-@EqualsAndHashCode
-@CompileStatic
-@Sortable(includes=['updated'])
-class Plugin implements Comparable{
- String name
- Owner owner
- String desc
- List<String> labels
- String latestVersion
- String vcsUrl
- LocalDateTime updated
- String license
- Integer githubStars
-
- static String getUpdated(Plugin plugin){
- plugin.updated
- }
-
-
- @Override
- int compareTo(@NotNull Object o) {
- return 0
- }
-
-}
diff --git a/buildSrc/src/main/groovy/org/grails/plugin/PluginsPage.groovy b/buildSrc/src/main/groovy/org/grails/plugin/PluginsPage.groovy
deleted file mode 100644
index 7e841c2aec6..00000000000
--- a/buildSrc/src/main/groovy/org/grails/plugin/PluginsPage.groovy
+++ /dev/null
@@ -1,328 +0,0 @@
-package org.grails.plugin
-
-import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import groovy.xml.MarkupBuilder
-import org.grails.guides.Category
-import org.grails.guides.TagUtils
-import org.grails.tags.Tag
-import org.grails.tags.TagCloud
-import java.time.format.DateTimeFormatter
-import java.util.stream.Collector
-import java.util.stream.Collectors
-
-@CompileStatic
-class PluginsPage {
- private static final Comparator<Plugin> COMPARE_BY_NAME = new Comparator<Plugin>() {
- @Override
- int compare(Plugin o1, Plugin o2) {
- o1.name.toLowerCase() <=> o2.name.toLowerCase()
- }
- }
-
- private static final Comparator<Plugin> COMPARE_BY_GITHUB_STARS = new Comparator<Plugin>() {
- @Override
- int compare(Plugin o1, Plugin o2) {
- o2.githubStars <=> o1.githubStars
- }
- }
- private static final Comparator<Plugin> COMPARE_BY_UPDATED = new Comparator<Plugin>() {
- @Override
- int compare(Plugin o1, Plugin o2) {
- o2.updated <=> o1.updated
- }
- }
- private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("MMM d, YYYY");
-
- @CompileDynamic
- static String mainContent(String siteUrl,
- List<Plugin> plugins,
- String title,
- List<Plugin> filteredPlugins) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div(class: 'headerbar chalicesbg') {
- div(class: 'content') {
- h1 'Grails Plugins'
- }
- }
- html.div(class: 'content') {
- if (title !== 'Grails Plugins') {
- html.div(class: "breadcrumbs") {
- a(href: siteUrl + "/plugins.html", "All Grails Plugins")
- span(' » ')
- span(title)
- }
- }
- div(class: 'twocolumns') {
- div(class: 'column') {
- mkp.yieldUnescaped searchBox(null, null)
- mkp.yieldUnescaped latestPlugins(siteUrl, plugins)
- mkp.yieldUnescaped topRatedPlugins(siteUrl, plugins)
- mkp.yieldUnescaped pluginsByTag(siteUrl, plugins)
- mkp.yieldUnescaped pluginsByOwner(siteUrl, plugins)
- mkp.yieldUnescaped linksMenu(siteUrl)
- }
- div(class: 'column') {
- if (filteredPlugins != null) {
- mkp.yieldUnescaped(renderPlugins(siteUrl, filteredPlugins, title))
- } else {
- mkp.yieldUnescaped(renderPlugins(siteUrl, plugins, title))
- }
- }
- }
-
- }
- writer.toString()
- }
-
-
- @CompileDynamic
- static String renderTwoColumnsPlugins(String siteUrl, List<Plugin> plugins) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div {
- h3(class: "columnheader", "Grails Plugins")
- plugins.sort(COMPARE_BY_NAME)
- int index = 0
- int page = 2
- do {
- List<Plugin> l = (plugins.size() > (index + page)) ?
- plugins.subList(index, index + page) :
- plugins.subList(index, plugins.size())
- l = l.reverse()
- if (l.size() == 1) {
- mkp.yieldUnescaped renderSinglePlugin(siteUrl, l[0])
- } else {
- div(class: 'twocolumns') {
- div(class: 'column') {
- if (l.size() >= 1) {
- mkp.yieldUnescaped renderPlugins(siteUrl, [l[0]])
- }
- }
- div(class: 'column') {
- if (l.size() >= 2) {
- mkp.yieldUnescaped renderPlugins(siteUrl, [l[1]])
- }
- }
- }
- }
- index += page
- } while (plugins.size() > index)
- }
- writer.toString()
- }
-
- @CompileDynamic
- static String linksMenu(String siteUrl) {
- List<Map<String, String>> links = [
- [url: "https://grails.apache.org/blog/2021-04-07-publish-grails-plugin-to-maven-central.html", title: "Publishing Guide"],
- [url: "https://github.com/apache/grails-plugins-metadata", title: "Portal on Github"],
- ]
-
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div {
- mkp.yieldUnescaped createHeader('Useful Link')
- ul(class: 'guidegroup') {
- for (Map<String, String> link : links) {
- li {
- a(href: link.url, link.title)
- }
- }
- }
- }
- writer.toString()
- }
-
- @CompileDynamic
- static String renderPlugins(String siteUrl, List<Plugin> plugins, String title) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- if (title != 'Grails Plugins') {
- html.h3(class: "columnheader allpluginslabel", "Plugins")
- } else {
- html.h3(class: "columnheader allpluginslabel", "All Grails Plugins (" +plugins.size() +")")
- }
- html.h3(class: "columnheader searchresultslabel hidden", "Plugins Filtered by: ") {
- html.span(class: "query-label")
- }
- html.div(class: 'plugins allplugins') {
- ul {
- for (plugin in plugins) {
- mkp.yieldUnescaped renderSinglePlugin(siteUrl, plugin)
- }
- }
- }
- html.div(class: "guidegroup noresults hidden") {
- div(class: "guidegroupheader") {
- h2("No results found!")
- }
- }
- html.div(class: 'searchresults hidden') {
- mkp.yieldUnescaped('')
- }
- html.div(class: 'pagination-container')
- writer.toString()
- }
-
- @CompileDynamic
- static String pluginsByOwner(String siteUrl, List<Plugin> plugins) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div {
- mkp.yieldUnescaped createHeader('Plugins By Owner')
- Set<Tag> tags = TagUtils.populateTagsByPluginsOwners(plugins)
- mkp.yieldUnescaped TagCloud.tagCloud(siteUrl + "/plugins/owners", tags, false)
- }
- writer.toString()
- }
-
- @CompileDynamic
- static String topRatedPlugins(String siteUrl, List<Plugin> plugins) {
- List<Plugin> pluginsWithStars = plugins.stream()
- .filter(p -> p.githubStars != null)
- .sorted(COMPARE_BY_GITHUB_STARS)
- .collect(Collectors.toList())
- List<Plugin> topRatedPlugins = []
- for (Plugin plugin : pluginsWithStars) {
- if (topRatedPlugins.stream().noneMatch(p -> p.vcsUrl == plugin.vcsUrl)) {
- topRatedPlugins.add(plugin)
- }
- }
- topRatedPlugins = topRatedPlugins.take(5)
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div {
- mkp.yieldUnescaped createHeader('Top Rated Plugins')
- div {
- ul(class: 'plugin') {
- for (Plugin plugin : topRatedPlugins) {
- mkp.yieldUnescaped renderSinglePlugin(siteUrl, plugin)
- }
- }
- }
- }
- writer.toString()
- }
-
- @CompileDynamic
- static String latestPlugins(String siteUrl, List<Plugin> plugins) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div {
- mkp.yieldUnescaped createHeader('Latest Plugins')
- mkp.yieldUnescaped fetchLatestPlugins(siteUrl, plugins)
- }
- writer.toString()
- }
-
- @CompileDynamic
- static String pluginsByTag(String siteUrl, List<Plugin> plugins) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div {
- mkp.yieldUnescaped createHeader('Plugins By Tag')
- Set<Tag> tags = TagUtils.populateTagsByPlugins(plugins)
- mkp.yieldUnescaped TagCloud.tagCloud(siteUrl + "/plugins/tags", tags, false)
- }
- writer.toString()
- }
-
- @CompileDynamic
- static String renderSinglePlugin(String siteUrl, Plugin plugin) {
- StringWriter writer = new StringWriter()
- MarkupBuilder mb = new MarkupBuilder(writer)
-
- mb.li(class: 'plugin') {
-
- if (plugin.vcsUrl) {
- h3(class: 'name') {
- a(href: plugin.vcsUrl, plugin.name)
- }
- } else {
- h3(class: 'name') {
- a(plugin.name)
- }
- }
- if (plugin.desc) {
- p(class: 'desc') { mkp.yield(plugin.desc) }
- }
-
- if (plugin.latestVersion) {
- span plugin.latestVersion
- }
- mkp.yield(" published ")
- mkp.yield(FORMATTER.format(plugin.updated))
- if (plugin.owner) {
- a(href: "[%url]/plugins/owners/${plugin.owner.name}.html") {
- mkp.yield("by " + plugin.owner.name)
- }
- }
-
- if (plugin.labels) {
- ul(class: 'labels') {
- for (String label : plugin.labels) {
- li(class: 'label') {
- a(href: "[%url]/plugins/tags/${label}.html") {
- mkp.yield(label)
- }
- }
- }
- }
- }
- if (plugin.githubStars) {
- div(class: 'githubstar') {
- span(class: 'star') {
- img(src: "${siteUrl}/images/small_githubstar.svg", height: 20)
- }
- span(class: 'count', plugin.githubStars)
- }
- }
- }
- writer.toString()
- }
-
- @CompileDynamic
- static String createHeader(String section) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.h3(class: 'columnheader', section)
- writer.toString()
- }
-
- @CompileDynamic
- static String fetchLatestPlugins(String siteUrl, List<Plugin> plugins) {
- final List<Plugin> sortedPlugins = plugins.stream()
- .sorted(COMPARE_BY_UPDATED)
- .collect(Collectors.toList())
- final List<Plugin> topFive = sortedPlugins.take(5)
- renderLatestPlugins(siteUrl, topFive)
- }
-
- @CompileDynamic
- static String renderLatestPlugins(String siteUrl, List<Plugin> topFive) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.ul(class: 'plugin') {
- for (plugin in topFive) {
- mkp.yieldUnescaped renderSinglePlugin(siteUrl, plugin)
- }
- }
- writer.toString()
- }
-
- @CompileDynamic
- static String searchBox(Tag tag, Category category) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- if (!(tag || category)) {
- html.div(class: 'searchbox', style: 'margin-top: 50px !important;') {
- div(class: 'search', style: 'margin-bottom: 0px !important;') {
- input(type: 'text', id: 'query', placeholder: 'SEARCH')
- }
- }
- }
- writer.toString()
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/questions/Question.groovy b/buildSrc/src/main/groovy/org/grails/questions/Question.groovy
deleted file mode 100644
index 5ba385d9d05..00000000000
--- a/buildSrc/src/main/groovy/org/grails/questions/Question.groovy
+++ /dev/null
@@ -1,9 +0,0 @@
-package org.grails.questions
-import groovy.transform.CompileStatic
-
-@CompileStatic
-class Question implements QuestionHtml {
- String title
- String answer
- String slug
-}
diff --git a/buildSrc/src/main/groovy/org/grails/questions/QuestionHtml.groovy b/buildSrc/src/main/groovy/org/grails/questions/QuestionHtml.groovy
deleted file mode 100644
index 3feca8132e7..00000000000
--- a/buildSrc/src/main/groovy/org/grails/questions/QuestionHtml.groovy
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.grails.questions
-
-import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import groovy.xml.MarkupBuilder
-import org.grails.PageElement
-import org.grails.utils.MarkdownUtils
-
-@CompileStatic
-trait QuestionHtml implements PageElement {
-
- abstract String getTitle()
- abstract String getAnswer()
- abstract String getSlug()
-
- @CompileDynamic
- @Override
- String renderAsHtml() {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div(class: 'question', id: slug) {
- h2 {
- html.mkp.yieldUnescaped title
- }
- String text = MarkdownUtils.htmlFromMarkdown(answer)
- text = text.replaceAll("\\\\n","<br/>")
- html.mkp.yieldUnescaped(text)
- }
- writer.toString()
- }
-
-}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/org/grails/questions/QuestionsPage.groovy b/buildSrc/src/main/groovy/org/grails/questions/QuestionsPage.groovy
deleted file mode 100644
index 57668de17e6..00000000000
--- a/buildSrc/src/main/groovy/org/grails/questions/QuestionsPage.groovy
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.grails.questions
-
-import groovy.xml.MarkupBuilder
-import org.grails.utils.MarkdownUtils
-import org.yaml.snakeyaml.Yaml
-
-class QuestionsPage {
-
- static String mainContent(File questions) {
- def writer = new StringWriter()
- def html = new MarkupBuilder(writer)
- def yaml = new Yaml()
- def model = yaml.load(questions.newDataInputStream())
- def questionList = model['questions'].collect {
- new Question(it as Map)
- }
- html.div(class: 'headerbar chalicesbg') {
- html.div(class: 'content') {
- h1('Questions')
- }
- }
- html.div(class: 'content') {
- article(id: 'questions') {
- for (Question question : questionList) {
- div(class: 'question', id: question.slug) {
- h2(class: 'columnheader', question.title)
- mkp.yieldUnescaped(
- MarkdownUtils.htmlFromMarkdown(question.answer)
- )
- }
- }
- }
- }
- writer.toString()
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/tags/Tag.groovy b/buildSrc/src/main/groovy/org/grails/tags/Tag.groovy
deleted file mode 100644
index 2143ad58f3a..00000000000
--- a/buildSrc/src/main/groovy/org/grails/tags/Tag.groovy
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.grails.tags
-
-import groovy.transform.Canonical
-import groovy.transform.CompileStatic
-import groovy.transform.Sortable
-
-@Canonical
-@Sortable(includes = ['title'])
-@CompileStatic
-class Tag {
- String title
- int ocurrence
-
- void setTitle(String title) {
- this.title = title?.trim()
- }
-
- String getSlug() {
- if ( !title ) {
- return null
- }
- String str = title
- str = str.toLowerCase()
- for ( String regex : [' ', '\\[', ']', '\''] ) {
- str = str.replaceAll(regex, '')
- }
- str = str.trim()
- URLEncoder.encode(str, 'UTF-8')
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/tags/TagCloud.groovy b/buildSrc/src/main/groovy/org/grails/tags/TagCloud.groovy
deleted file mode 100644
index 35200e2de5a..00000000000
--- a/buildSrc/src/main/groovy/org/grails/tags/TagCloud.groovy
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.grails.tags
-
-import groovy.transform.CompileDynamic
-import groovy.transform.CompileStatic
-import groovy.xml.MarkupBuilder
-
-@CompileStatic
-class TagCloud {
-
- @CompileDynamic
- static String tagCloud(String url, Set<Tag> tags, boolean includeHeader = true) {
- StringWriter writer = new StringWriter()
- MarkupBuilder html = new MarkupBuilder(writer)
- html.div(class: 'tagsbytopic') {
- if (includeHeader) {
- h3 class: 'columnheader', 'Guides by Tag'
- }
- ul(class: 'tagcloud') {
- tags.sort { Tag a, Tag b -> a.slug <=> b.slug }.each { Tag t ->
- li(class: "tag${t.ocurrence}") {
- a href: "${url}/${t.slug.toLowerCase()}.html", t.title
- }
- }
- }
- }
- writer.toString()
- }
-}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/GrailsWebsiteExtension.groovy b/buildSrc/src/main/groovy/website/gradle/GrailsWebsiteExtension.groovy
similarity index 95%
rename from buildSrc/src/main/groovy/org/grails/gradle/GrailsWebsiteExtension.groovy
rename to buildSrc/src/main/groovy/website/gradle/GrailsWebsiteExtension.groovy
index c99987636b5..2ae07cecaff 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/GrailsWebsiteExtension.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/GrailsWebsiteExtension.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle
+package website.gradle
import javax.inject.Inject
@@ -35,6 +35,7 @@ class GrailsWebsiteExtension {
public static final String NAME = 'grailsWebsite'
+ final RegularFileProperty htaccess
final RegularFileProperty modules
final RegularFileProperty profiles
final RegularFileProperty questions
@@ -57,6 +58,9 @@ class GrailsWebsiteExtension {
@Inject
GrailsWebsiteExtension(ObjectFactory objects, ProjectLayout layout, ProviderFactory providers) {
// File properties with conventions
+ htaccess = objects.fileProperty()
+ htaccess.convention(layout.buildDirectory.file('dist/.htaccess'))
+
modules = objects.fileProperty()
modules.convention(layout.projectDirectory.file('conf/modules.yml'))
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/GrailsWebsitePlugin.groovy b/buildSrc/src/main/groovy/website/gradle/GrailsWebsitePlugin.groovy
similarity index 64%
rename from buildSrc/src/main/groovy/org/grails/gradle/GrailsWebsitePlugin.groovy
rename to buildSrc/src/main/groovy/website/gradle/GrailsWebsitePlugin.groovy
index 76929e1bcb5..94d3439d5f2 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/GrailsWebsitePlugin.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/GrailsWebsitePlugin.groovy
@@ -16,32 +16,31 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle
+package website.gradle
import groovy.transform.CompileStatic
import org.gradle.api.Plugin
import org.gradle.api.Project
-import org.grails.gradle.tasks.BlogTask
-import org.grails.gradle.tasks.BuildGuidesTask
-import org.grails.gradle.tasks.CopyAssetsTask
-import org.grails.gradle.tasks.DocumentationTask
-import org.grails.gradle.tasks.DownloadTask
-import org.grails.gradle.tasks.GuidesTask
-import org.grails.gradle.tasks.HtaccessTask
-import org.grails.gradle.tasks.MinutesTask
-import org.grails.gradle.tasks.PluginsTask
-import org.grails.gradle.tasks.ProfilesTask
-import org.grails.gradle.tasks.QuestionsTask
-import org.grails.gradle.tasks.RenderSiteTask
-import org.grails.gradle.tasks.SitemapTask
+import website.gradle.tasks.BlogTask
+
+import website.gradle.tasks.AssetsTask
+import website.gradle.tasks.DocumentationTask
+import website.gradle.tasks.DownloadTask
+import website.gradle.tasks.GrailsWebsiteTask
+import website.gradle.tasks.GuidesTask
+import website.gradle.tasks.HtaccessTask
+import website.gradle.tasks.MinutesTask
+import website.gradle.tasks.PluginsTask
+import website.gradle.tasks.ProfilesTask
+import website.gradle.tasks.QuestionsTask
+import website.gradle.tasks.RenderSiteTask
+import website.gradle.tasks.SitemapTask
@CompileStatic
class GrailsWebsitePlugin implements Plugin<Project> {
- public static final String TASK_RENDER_SITE = 'renderSite'
-
@Override
void apply(Project project) {
project.pluginManager.apply('base')
@@ -51,8 +50,8 @@ class GrailsWebsitePlugin implements Plugin<Project> {
GrailsWebsiteExtension
)
+ AssetsTask.register(project, siteExt)
BlogTask.register(project, siteExt)
- CopyAssetsTask.register(project, siteExt)
DocumentationTask.register(project, siteExt)
DownloadTask.register(project, siteExt)
GuidesTask.register(project, siteExt)
@@ -63,36 +62,47 @@ class GrailsWebsitePlugin implements Plugin<Project> {
QuestionsTask.register(project, siteExt)
SitemapTask.register(project, siteExt).configure {
+
// SitemapTask must run after all tasks that generate files in dist/
+
it.dependsOn(BlogTask.NAME)
it.dependsOn(MinutesTask.NAME)
it.dependsOn(PluginsTask.NAME)
it.dependsOn(HtaccessTask.NAME)
}
- BuildGuidesTask.register(project, siteExt).configure {
- it.dependsOn(CopyAssetsTask.NAME)
- it.dependsOn(GuidesTask.NAME)
- it.finalizedBy(SitemapTask.NAME)
+ RenderSiteTask.register(project, siteExt).configure {
- }
+ // The Grails Guides are not part of this site, they are published to https://guides.grails.org
+ // and hosted at gh-pages in https://github.com/grails/grails-guides-template
+ // The buildGuides task can be used to generate the guide site.
- RenderSiteTask.register(project, siteExt).configure {
- it.dependsOn(CopyAssetsTask.NAME)
+ it.dependsOn(AssetsTask.NAME)
it.dependsOn(DocumentationTask.NAME)
it.dependsOn(DownloadTask.NAME)
- it.dependsOn(GuidesTask.NAME)
it.dependsOn(ProfilesTask.NAME)
it.dependsOn(QuestionsTask.NAME)
+
it.finalizedBy(BlogTask.NAME)
- it.finalizedBy(PluginsTask.NAME)
it.finalizedBy(MinutesTask.NAME)
- it.finalizedBy(SitemapTask.NAME)
it.finalizedBy(HtaccessTask.NAME)
+ it.finalizedBy(PluginsTask.NAME)
+ it.finalizedBy(SitemapTask.NAME)
+ }
+
+ project.tasks.register('buildGuides') {
+ // Task for only generating the Grails Guides which are published to https://guides.grails.org
+ // and hosted at gh-pages in https://github.com/grails/grails-guides-template
+ it.description = 'Build guides website - generates guides pages, copies assets and generates a sitemap'
+ it.group = GrailsWebsiteTask.GROUP
+ it.dependsOn(AssetsTask.NAME)
+ it.dependsOn(GuidesTask.NAME)
+ it.finalizedBy(SitemapTask.NAME)
+
}
project.tasks.named('build') {
- it.dependsOn(TASK_RENDER_SITE)
+ it.dependsOn(RenderSiteTask.NAME)
}
}
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/QuestionsTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/AssetsTask.groovy
similarity index 53%
copy from buildSrc/src/main/groovy/org/grails/gradle/tasks/QuestionsTask.groovy
copy to buildSrc/src/main/groovy/website/gradle/tasks/AssetsTask.groovy
index 18c4be1fb8e..96f6d886aed 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/QuestionsTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/AssetsTask.groovy
@@ -16,18 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import javax.inject.Inject
import groovy.transform.CompileStatic
import org.gradle.api.Project
+import org.gradle.api.file.CopySpec
import org.gradle.api.file.DirectoryProperty
-import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.CacheableTask
-import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
@@ -35,53 +35,57 @@ import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.grails.gradle.GrailsWebsiteExtension
-import org.grails.questions.QuestionsPage
+import website.gradle.GrailsWebsiteExtension
@CompileStatic
@CacheableTask
-class QuestionsTask extends GrailsWebsiteTask {
+class AssetsTask extends GrailsWebsiteTask {
@Internal
- String description = 'Generates FAQ HTML - build/temp/faq.html'
+ final String description =
+ 'Copies css, js, fonts and images from the assets folder to the dist folder'
- public static final String NAME = 'genFaq'
+ public static final String NAME = 'copyAssets'
- private static final String PAGE_NAME_QUESTIONS = 'faq.html'
+ // dir name -> list of file name glob filters
+ static final Map<String,List<String>> assetTypes = [
+ 'files': ['*.jar', '*.md5', '*.pdf'],
+ 'fonts': ['*.eot', '*.ttf', '*.woff', '*.woff2'],
+ 'images': ['*.gif', '*.ico', '*.jpeg', '*.jpg', '*.png', '*.svg'],
+ 'javascripts': ['*.js'],
+ 'stylesheets': ['*.css'],
+ ]
private final ObjectFactory objects
@Inject
- QuestionsTask(ObjectFactory objects) {
+ AssetsTask(ObjectFactory objects) {
this.objects = objects
}
- @InputFile
+ @InputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
- final RegularFileProperty questions = objects.fileProperty()
+ final DirectoryProperty assetsDir = objects.directoryProperty()
@OutputDirectory
final DirectoryProperty outputDir = objects.directoryProperty()
- static TaskProvider<QuestionsTask> register(
- Project project,
- GrailsWebsiteExtension siteExt,
- String name = NAME
- ) {
- project.tasks.register(name, QuestionsTask) {
+ static TaskProvider<AssetsTask> register(Project project, GrailsWebsiteExtension siteExt) {
+ project.tasks.register(NAME, AssetsTask) {
it.outputDir.set(siteExt.outputDir)
- it.questions.set(siteExt.questions)
+ it.assetsDir.set(siteExt.assetsDir)
}
}
@TaskAction
- void renderQuestionsPage() {
- def buildDir = outputDir.get().asFile
- def temp = new File(buildDir, 'temp').tap { it.mkdir() }
-
- def output = new File(temp, PAGE_NAME_QUESTIONS)
- output.createNewFile()
- output.text = "title: FAQ | Apache Grails®\nbody: faq\n---\n" +
- QuestionsPage.mainContent(questions.get().asFile)
+ void copyAssets() {
+ assetTypes.each { dirName, extFilters ->
+ project.copy { CopySpec cs ->
+ cs.from(assetsDir.dir(dirName))
+ cs.into(outputDir.dir("dist/$dirName"))
+ cs.include(extFilters.collect { '**/' + it })
+ cs.includeEmptyDirs = false
+ }
+ }
}
}
diff --git a/buildSrc/src/main/groovy/website/gradle/tasks/BlogTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/BlogTask.groovy
new file mode 100644
index 00000000000..64b6843565f
--- /dev/null
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/BlogTask.groovy
@@ -0,0 +1,523 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package website.gradle.tasks
+
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.util.regex.Pattern
+
+import javax.annotation.Nonnull
+import javax.inject.Inject
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.xml.MarkupBuilder
+
+import io.micronaut.rss.DefaultRssFeedRenderer
+import io.micronaut.rss.RssChannel
+import io.micronaut.rss.RssItem
+import org.gradle.api.Project
+import org.gradle.api.file.CopySpec
+import org.gradle.api.file.Directory
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.TaskProvider
+
+import website.model.HtmlPost
+import website.model.MarkdownPost
+import website.model.PostMetadataAdapter
+import website.model.documentation.SiteMap
+import website.gradle.GrailsWebsiteExtension
+import website.utils.DateUtils
+import website.utils.MarkdownUtils
+
+@CompileStatic
+@CacheableTask
+class BlogTask extends GrailsWebsiteTask {
+
+ @Internal
+ final String description =
+ 'Renders Markdown posts (posts/*.md) into HTML pages (dist/blog/*.html). ' +
+ 'It generates tag pages. Generates RSS feed. ' +
+ 'Posts with future dates are not generated.'
+
+ public static final String NAME = 'renderBlog'
+
+ private static final String BLOG = 'blog'
+ private static final String IMAGES = 'images'
+ private static final String INDEX = 'index.html'
+ private static final String RSS_FILE = 'rss.xml'
+ private static final String TAG = 'tag'
+
+ private static final Pattern HASHTAG_SPAN = ~/<span class="hashtag">#(.*?)<\/span>/
+
+ private static List<String> ALLOWED_TAG_PREFIXES =
+ (('A'..'Z') + ('a'..'z') + ('0'..'9')).collect { '#' + it }
+
+ private static final int MAX_RELATED_POSTS = 3
+ private static final int MAX_TITLE_LENGTH = 45
+
+ private final ObjectFactory objects
+
+ @Inject
+ BlogTask(ObjectFactory objects) {
+ this.objects = objects
+ }
+
+ @InputFile
+ @PathSensitive(PathSensitivity.RELATIVE)
+ final RegularFileProperty document = objects.fileProperty()
+
+ @InputFile
+ @PathSensitive(PathSensitivity.RELATIVE)
+ final RegularFileProperty releases = objects.fileProperty()
+
+ @InputDirectory
+ @PathSensitive(PathSensitivity.RELATIVE)
+ final DirectoryProperty assetsDir = objects.directoryProperty()
+
+ @InputDirectory
+ @PathSensitive(PathSensitivity.RELATIVE)
+ final DirectoryProperty postsDir = objects.directoryProperty()
+
+ @Input
+ final Property<String> about = objects.property(String)
+
+ @Input
+ final ListProperty<String> keywords = objects.listProperty(String)
+
+ @Input
+ final Property<String> robots = objects.property(String)
+
+ @Input
+ final Property<String> title = objects.property(String)
+
+ @Input
+ final Property<String> url = objects.property(String)
+
+ @OutputDirectory
+ final DirectoryProperty outputDir = objects.directoryProperty()
+
+ static TaskProvider<BlogTask> register(Project project, GrailsWebsiteExtension siteExt) {
+ project.tasks.register(NAME, BlogTask) {
+ it.about.set(siteExt.description)
+ it.assetsDir.set(siteExt.assetsDir)
+ it.document.set(siteExt.template)
+ it.keywords.set(siteExt.keywords)
+ it.outputDir.set(siteExt.outputDir)
+ it.postsDir.set(siteExt.postsDir)
+ it.releases.set(siteExt.releases)
+ it.robots.set(siteExt.robots)
+ it.title.set(siteExt.title)
+ it.url.set(siteExt.url)
+ }
+ }
+
+ @TaskAction
+ void renderBlog() {
+ def releasesDir = releases.get().asFile
+ def today = new Date()
+ def versionsOptions = SiteMap.olderVersions(releasesDir)
+ .reverse()
+ .collect { "<option>$it</option>" }
+ .join(' ')
+ def meta = RenderSiteTask.siteMeta(
+ title.get(),
+ about.get(),
+ url.get(),
+ keywords.get(),
+ robots.get(),
+ SiteMap.latestVersion(releasesDir).versionText,
+ versionsOptions
+ )
+ def posts = parsePosts(postsDir.get().asFile)
+ .findAll { !it.parsedDate.after(today) }
+ .sort(false)
+ def processed = processPosts(meta, posts)
+ def blogOut = outputDir.dir('dist/blog').get().asFile.tap { it.mkdirs() }
+ def templateText = document.get().asFile.text
+
+ renderPosts(meta, processed, blogOut, templateText)
+
+ copyImages(assetsDir.dir('bgimages'), outputDir.dir('dist/images'))
+ copyImages(postsDir, outputDir.dir('dist/blog'))
+ }
+
+ void copyImages(Provider<Directory> srcDir, Provider<Directory> dstDir) {
+ project.copy { CopySpec copy ->
+ copy.from(srcDir)
+ copy.into(dstDir)
+ copy.include(AssetsTask.assetTypes.images)
+ }
+ }
+
+ static RssItem rssItemWithPage(
+ String title,
+ Date pubDate,
+ String link,
+ String guid,
+ String html,
+ String author = null
+ ) {
+ def description = html.contains('<span class="date">')
+ ? html.substring(html.indexOf('<span class="date">'))
+ .with { it.substring(it.indexOf('</span>') + '</span>'.length()) }
+ : html
+ def zdt = Instant.ofEpochMilli(pubDate.time)
+ .atZone(ZoneId.systemDefault())
+ .withZoneSameInstant(ZoneId.of('GMT'))
+ def builder = RssItem.builder()
+ .title(title)
+ .pubDate(zdt)
+ .link(link)
+ .guid(guid)
+ .description(description)
+ if (author) {
+ builder.author(parseAuthorName(author))
+ }
+ builder.build()
+ }
+
+ static String parseAuthorName(String author) {
+ author.split(/\s*\(/, 2)[0].trim()
+ }
+
+ @CompileDynamic
+ static String renderPostHtml(HtmlPost htmlPost, String templateText, List<HtmlPost> posts) {
+ def writer = new StringWriter()
+ new MarkupBuilder(writer).with {
+ div(class: 'header-bar chalices-bg') {
+ div(class: 'content') {
+ h1 {
+ a(href: '[%url]/blog/index.html', 'Grails Blog')
+ }
+ }
+ }
+ div(class: 'content container') {
+ div(class: 'light padded blog-post') {
+ mkp.yieldUnescaped(htmlPost.html)
+ h2(class: 'space-above') {
+ span('You might also like ...')
+ }
+ div(class: 'three-columns') {
+ relatedPosts(htmlPost, posts).each { post ->
+ div(class: 'column') {
+ mkp.yieldUnescaped(BlogTask.postCard(post))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ def metadata = htmlPost.metadata.toMap()
+ def html = RenderSiteTask.renderHtmlWithTemplateContent(writer.toString(), metadata, templateText)
+ html = RenderSiteTask.highlightMenu(html, metadata, htmlPost.path)
+
+ def bodyClass = metadata.body
+ bodyClass ? html.replace('<body>', "<body class='${bodyClass}'>") : html
+ }
+
+ static List<HtmlPost> relatedPosts(HtmlPost htmlPost, List<HtmlPost> posts) {
+ def byPath = new LinkedHashMap<String, HtmlPost>() // preserves insertion order
+
+ // 1) Prefer posts that share tags (in tag iteration order)
+ htmlPost.tags.each { tag ->
+ posts.findAll { it.path != htmlPost.path && it.tags.contains(tag) }
+ .each { p ->
+ if (byPath.size() < MAX_RELATED_POSTS) byPath.putIfAbsent(p.path, p)
+ }
+ }
+
+ // 2) Fill remaining slots with other posts (original order)
+ if (byPath.size() < MAX_RELATED_POSTS) {
+ posts.each { post ->
+ if (post.path != htmlPost.path && byPath.size() < MAX_RELATED_POSTS) {
+ byPath.putIfAbsent(post.path, post)
+ }
+ }
+ }
+
+ // 3) Return newest first (and cap)
+ byPath.values()
+ .toList()
+ .sort { DateUtils.parseDate(it.metadata.date) }
+ .reverse()
+ .take(MAX_RELATED_POSTS)
+ }
+
+ static List<HtmlPost> processPosts(Map<String, String> globalMetadata, List<MarkdownPost> markdownPosts) {
+ markdownPosts.collect { post ->
+ def metadata = RenderSiteTask.processMetadata(globalMetadata + post.metadata)
+ def postMetadata = new PostMetadataAdapter(metadata)
+ def markdown = post.content
+ if (metadata.slides) markdown += "\n\n[Slides](${metadata.slides})\n\n"
+ if (metadata.code) markdown += "\n\n[Code](${metadata.code})\n\n"
+ def html = MarkdownUtils.htmlFromMarkdown(markdown)
+ RenderSiteTask.parseVideoIframe(metadata)?.with { iframe ->
+ html += iframe
+ }
+ def contentHtml = wrapTags(metadata, html)
+ new HtmlPost(
+ metadata: postMetadata,
+ html: contentHtml,
+ path: post.path,
+ tags: parseTags(contentHtml)
+ )
+ }
+ }
+
+ static void renderPosts(
+ Map<String, String> globalMetadata,
+ List<HtmlPost> listOfPosts,
+ File outputDir,
+ String templateText
+ ) {
+ List<String> postCards = []
+ List<RssItem> rssItems = []
+ Map<String, Integer> tagsCount = [:].withDefault { 0 }
+ Map<String, List<String>> tagPosts = [:].withDefault { [] }
+
+ listOfPosts.each { post ->
+ postCards << postCard(post)
+ def html = renderPostHtml(post, templateText, listOfPosts)
+ new File(outputDir, post.path).tap {
+ it.parentFile?.mkdirs()
+ it.setText(text, 'UTF-8')
+ }
+ parseTags(html).each { tag ->
+ tagsCount[tag] = tagsCount[tag] + 1
+ tagPosts[tag] << post.path
+ }
+ def link = postLink(post)
+ rssItems << rssItemWithPage(
+ post.metadata.title,
+ DateUtils.parseDate(post.metadata.date),
+ link,
+ post.path.replace('.html', ''),
+ post.html,
+ post.metadata.author
+ )
+ }
+ renderArchive(new File(outputDir, INDEX), postCards, globalMetadata, templateText)
+ renderRss(globalMetadata, rssItems, new File(outputDir.parentFile, RSS_FILE))
+ renderTags(globalMetadata, outputDir, tagsCount.keySet(), listOfPosts, templateText)
+ }
+
+ static Set<String> parseTags(String html) {
+ def tags = [] as Set<String>
+ def m = (html =~ HASHTAG_SPAN)
+ while (m.find()) {
+ tags.add(m.group(1))
+ }
+ tags
+ }
+
+ static void renderTags(
+ Map<String, String> metadata,
+ File outputDir,
+ Set<String> tags,
+ List<HtmlPost> posts,
+ String templateText
+ ) {
+ def tagFolder = new File(outputDir, TAG).tap { mkdirs() }
+ def meta = RenderSiteTask.processMetadata(metadata)
+ tags.each { tag ->
+ def tagCards = posts
+ .findAll { it.tags.contains(tag) }
+ .collect { postCard(it) }
+ renderCards(
+ new File(tagFolder, "${tag}.html"),
+ tagCards,
+ new LinkedHashMap(meta).tap {
+ it.title = tag.toUpperCase() + ' | Blog | Grails Framework'
+ },
+ templateText,
+ renderTagTitle(tag)
+ )
+ }
+ }
+
+ static String postLink(HtmlPost post) {
+ "$post.metadata.url/$BLOG/$post.path"
+ }
+
+ @CompileDynamic
+ static String postCard(HtmlPost htmlPost) {
+ def meta = htmlPost.metadata
+ def imageUrl = meta['image'] ? "$meta.url/$IMAGES/${meta['image']}" : null
+ def style = imageUrl ? "background-image: url('$imageUrl')" : null
+ def title = RenderSiteTask.replaceLineWithMetadata(meta.title, meta.toMap())
+ title = title.size() > MAX_TITLE_LENGTH ? "${title.take(MAX_TITLE_LENGTH)}..." : title
+ def writer = new StringWriter()
+ new MarkupBuilder(writer).with {
+ omitNullAttributes = true
+ article(class: 'blog-card', style: style) {
+ a(href: postLink(htmlPost)) {
+ h3 { mkp.yield(RenderSiteTask.formatDate(meta.date)) }
+ h2 { mkp.yield(title) }
+ }
+ }
+ }
+ writer.toString()
+ }
+
+ @CompileDynamic
+ private static String renderTagTitle(String tag) {
+ def writer = new StringWriter()
+ new MarkupBuilder(writer).with {
+ h1 {
+ span('Tag:')
+ b(tag)
+ }
+ }
+ writer.toString()
+ }
+
+ @CompileDynamic
+ private static void renderArchive(
+ File f,
+ List<String> postCards,
+ Map<String, String> siteMeta,
+ String templateText
+ ) {
+ def meta = RenderSiteTask.processMetadata(siteMeta).tap {
+ it.title = 'Blog | Grails Framework'
+ }
+ def html = cardsHtml(postCards)
+ html = RenderSiteTask.renderHtmlWithTemplateContent(html, meta, templateText)
+ html = RenderSiteTask.highlightMenu(html, meta, "/$BLOG/$INDEX")
+ f.setText(html, 'UTF-8')
+ }
+
+ private static void renderCards(
+ File f,
+ List<String> cards,
+ Map<String, String> meta,
+ String templateText,
+ String title = null
+ ) {
+ f.setText(
+ RenderSiteTask.renderHtmlWithTemplateContent(
+ cardsHtml(cards, title),
+ meta,
+ templateText
+ ),
+ 'UTF-8'
+ )
+ }
+
+ @CompileDynamic
+ static String cardsHtml(List<String> cards, String title = null) {
+ def writer = new StringWriter()
+ new MarkupBuilder(writer).tap {
+ div(class: 'header-bar chalices-bg') {
+ div(class: 'content') {
+ if (title) {
+ mkp.yieldUnescaped(title)
+ } else {
+ h1 { a(href: '[%url]/blog/index.html', 'Grails Blog') }
+ }
+ }
+ }
+ div(class: 'clear content container') {
+ div(class: 'light') {
+ div(class: 'padded', style: 'padding-top: 0') {
+ cards.collate(3).each { row ->
+ div(class: 'three-columns') {
+ row.each { card ->
+ div(class: 'column') { mkp.yieldUnescaped(card) }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ writer.toString()
+ }
+
+ private static void renderRss(
+ Map<String, String> sitemeta,
+ List<RssItem> rssItems,
+ File outputFile
+ ) {
+ def nowGmt = ZonedDateTime.now(ZoneId.of('GMT'))
+ def channel = RssChannel.builder(sitemeta.title, sitemeta.url, sitemeta.description)
+ .pubDate(nowGmt)
+ .lastBuildDate(nowGmt)
+ .docs('https://blogs.law.harvard.edu/tech/rss')
+ .generator('Micronaut RSS')
+ .managingEditor('private@grails.apache.org')
+ .webMaster('private@grails.apache.org')
+ .tap { b -> rssItems.each { b.item(it) } }
+ .build()
+ outputFile.withWriter { writer ->
+ new DefaultRssFeedRenderer().render(writer, channel)
+ }
+ }
+
+ static boolean isTag(String word) {
+ ALLOWED_TAG_PREFIXES.any {word.startsWith(it) }
+ }
+
+ @Nonnull
+ static String wrapTags(Map<String, String> metadata, @Nonnull String html) {
+ def base = "$metadata.url/$BLOG/$TAG"
+ html.readLines().collect { line ->
+ if (!(line.startsWith('<p>') && line.endsWith('</p>'))) return line
+
+ def inner = line.replaceFirst(/^<p>/, '').replaceFirst(/<\/p>$/, '')
+ def replaced = inner.split(' ').collect { word ->
+ if (!isTag(word)) return word
+ def tag = word.tokenize('<')[0] // "#foo" from "#foo</span>" etc
+ def slug = tag.replace('#', '')
+ """<a href="$base/${slug}.html"><span class="hashtag">$tag</span></a>"""
+ }.join(' ')
+ "<p>$replaced</p>"
+ }.join('\n')
+ }
+
+ static List<MarkdownPost> parsePosts(File postsDir) {
+ postsDir.listFiles()
+ .findAll { it.name ==~ /.*\.(md|markdown)$/ }
+ .collect { file ->
+ def cm = RenderSiteTask.parseFile(file)
+ new MarkdownPost(
+ filename: file.name,
+ content: cm.content,
+ metadata: cm.metadata
+ )
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/DocumentationTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/DocumentationTask.groovy
similarity index 88%
rename from buildSrc/src/main/groovy/org/grails/gradle/tasks/DocumentationTask.groovy
rename to buildSrc/src/main/groovy/website/gradle/tasks/DocumentationTask.groovy
index c1e58520d75..a5bb653b3c0 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/DocumentationTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/DocumentationTask.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import javax.inject.Inject
@@ -37,15 +37,16 @@ import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.grails.documentation.DocumentationPage
-import org.grails.gradle.GrailsWebsiteExtension
+import website.model.documentation.DocumentationPage
+import website.gradle.GrailsWebsiteExtension
@CompileStatic
@CacheableTask
class DocumentationTask extends GrailsWebsiteTask {
@Internal
- String description = 'Generates the Documentation HTML Page -> build/temp/documentation.html'
+ final String description =
+ 'Generates the Documentation HTML Page -> build/temp/documentation.html'
public static final String NAME = 'genDocsPage'
@@ -82,15 +83,17 @@ class DocumentationTask extends GrailsWebsiteTask {
@TaskAction
void renderDocsPage() {
def buildDir = outputDir.get().asFile
- def tempDir = new File(buildDir, 'temp').tap {mkdir() }
- def outputFile = new File(tempDir, 'documentation.html').tap { createNewFile() }
- outputFile.text =
+ def tempDir = new File(buildDir, 'temp').tap {mkdirs() }
+ def outputFile = new File(tempDir, 'documentation.html')
+ outputFile.setText(
'title: Documentation | Grails Framework\n' +
'body: docs\n' +
'---\n' +
DocumentationPage.mainContent(
releases.get().asFile,
modules.get().asFile
- )
+ ),
+ 'UTF-8'
+ )
}
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/DownloadTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/DownloadTask.groovy
similarity index 86%
rename from buildSrc/src/main/groovy/org/grails/gradle/tasks/DownloadTask.groovy
rename to buildSrc/src/main/groovy/website/gradle/tasks/DownloadTask.groovy
index d8d5d5d7cd6..d0c2791bca1 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/DownloadTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/DownloadTask.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import javax.inject.Inject
@@ -37,15 +37,15 @@ import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.grails.documentation.DownloadPage
-import org.grails.gradle.GrailsWebsiteExtension
+import website.model.documentation.DownloadPage
+import website.gradle.GrailsWebsiteExtension
@CompileStatic
@CacheableTask
class DownloadTask extends GrailsWebsiteTask {
@Internal
- String description = 'Generates download HTML page -> build/temp/download.html'
+ final String description = 'Generates download HTML page -> build/temp/download.html'
public static final String NAME = 'genDownloads'
@@ -80,12 +80,14 @@ class DownloadTask extends GrailsWebsiteTask {
@TaskAction
void renderDocsPage() {
- def tempDir = new File(outputDir.get().asFile, 'temp').tap { it.mkdir() }
- def outputFile = new File(tempDir, 'download.html').tap { it.createNewFile() }
- outputFile.text =
+ def tempDir = new File(outputDir.get().asFile, 'temp').tap { it.mkdirs() }
+ def outputFile = new File(tempDir, 'download.html')
+ outputFile.setText(
'title: Downloads | Apache Grails®\n' +
'body: docs\n' +
'---\n' +
- DownloadPage.mainContent(releases.get().asFile)
+ DownloadPage.mainContent(releases.get().asFile),
+ 'UTF-8'
+ )
}
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/GrailsWebsiteTask.groovy
similarity index 89%
copy from buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
copy to buildSrc/src/main/groovy/website/gradle/tasks/GrailsWebsiteTask.groovy
index 6c7fa74fa36..49dbdf0e3c5 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/GrailsWebsiteTask.groovy
@@ -16,11 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import org.gradle.api.DefaultTask
abstract class GrailsWebsiteTask extends DefaultTask {
- String group = 'grails website'
+ public static String GROUP = 'grails website'
+
+ String group = GROUP
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GuidesTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/GuidesTask.groovy
similarity index 79%
rename from buildSrc/src/main/groovy/org/grails/gradle/tasks/GuidesTask.groovy
rename to buildSrc/src/main/groovy/website/gradle/tasks/GuidesTask.groovy
index fc87d95ea72..49fe3eb1b48 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GuidesTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/GuidesTask.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import javax.inject.Inject
@@ -38,19 +38,20 @@ import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.grails.Page
-import org.grails.documentation.SiteMap
-import org.grails.gradle.GrailsWebsiteExtension
-import org.grails.guides.GuidesFetcher
-import org.grails.guides.GuidesPage
-import org.grails.guides.TagUtils
+import website.model.Page
+import website.model.documentation.SiteMap
+import website.gradle.GrailsWebsiteExtension
+import website.model.guides.GuidesFetcher
+import website.model.guides.GuidesPage
+import website.model.guides.TagUtils
@CompileStatic
@CacheableTask
class GuidesTask extends GrailsWebsiteTask {
@Internal
- String description = 'Generates guides home, tags and categories HTML pages - build/temp/index.html'
+ final String description =
+ 'Generates guides home, tags and categories HTML pages - build/temp/index.html'
public static final String NAME = 'genGuides'
@@ -109,9 +110,8 @@ class GuidesTask extends GrailsWebsiteTask {
@TaskAction
void renderGuides() {
def tempDir = new File(outputDir.get().asFile, 'temp').tap { it.mkdirs() }
- def classLoader = getClass().classLoader
+ generateGuidesPages(tempDir, url.get())
- generateGuidesPages(classLoader, tempDir, url.get())
def template = document.get().asFile
def templateText = template.text
def distDir = new File(outputDir.get().asFile, 'dist').tap { it.mkdirs() }
@@ -171,29 +171,34 @@ class GuidesTask extends GrailsWebsiteTask {
)
}
- static void generateGuidesPages(ClassLoader classLoader, File pages, String url) {
+ static void generateGuidesPages(File pages, String url) {
def guides = GuidesFetcher.fetchGuides()
def tags = TagUtils.populateTags(guides)
- def pageOutput = new File(pages, PAGE_NAME_GUIDES)
- pageOutput.createNewFile()
- pageOutput.text = "title: Guides | Grails Framework\nbody: guides\nJAVASCRIPT: ${url}/javascripts/search.js\n---\n" +
- GuidesPage.mainContent(classLoader, guides, tags)
+ new File(pages, PAGE_NAME_GUIDES).setText(
+ "title: Guides | Grails Framework\nbody: guides\nJAVASCRIPT: $url/javascripts/search.js\n---\n" +
+ GuidesPage.mainContent(guides, tags),
+ 'UTF-8'
- def tagsDir = new File(pages, 'tags').tap { it.mkdir() }
+ )
+
+ def tagsDir = new File(pages, 'tags').tap { it.mkdirs() }
for (def tag : tags) {
def slug = "${tag.slug.toLowerCase()}.html"
- pageOutput = new File(tagsDir, slug)
- pageOutput.createNewFile()
- pageOutput.text = "---\ntitle: Guides with tag: ${tag} | Grails Framework\nbody: guides\n---\n" +
- GuidesPage.mainContent(classLoader, guides, tags, null, tag)
+ new File(tagsDir, slug).setText(
+ "---\ntitle: Guides with tag: $tag.title | Grails Framework\nbody: guides\n---\n" +
+ GuidesPage.mainContent(guides, tags, null, tag),
+ 'UTF-8'
+
+ )
}
- def categoriesDir = new File(pages, 'categories').tap { it.mkdir() }
+ def categoriesDir = new File(pages, 'categories').tap { it.mkdirs() }
for (def category : GuidesPage.categories.values()) {
def slug = "${category.slug.toLowerCase()}.html"
- pageOutput = new File(categoriesDir, slug)
- pageOutput.createNewFile()
- pageOutput.text = "---\ntitle: Guides at category ${category.name} | Grails Framework\nbody: guides\n---\n" +
- GuidesPage.mainContent(classLoader, guides, tags, category, null)
+ new File(categoriesDir, slug).setText(
+ "---\ntitle: Guides at category $category.name | Grails Framework\nbody: guides\n---\n" +
+ GuidesPage.mainContent(guides, tags, category, null),
+ 'UTF-8'
+ )
}
}
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/HtaccessTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/HtaccessTask.groovy
similarity index 72%
rename from buildSrc/src/main/groovy/org/grails/gradle/tasks/HtaccessTask.groovy
rename to buildSrc/src/main/groovy/website/gradle/tasks/HtaccessTask.groovy
index e3320b6438b..383da218e1b 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/HtaccessTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/HtaccessTask.groovy
@@ -16,34 +16,29 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import javax.inject.Inject
import groovy.transform.CompileStatic
import org.gradle.api.Project
-import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.CacheableTask
-import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Internal
-import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
-import org.gradle.api.tasks.PathSensitive
-import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.grails.gradle.GrailsWebsiteExtension
+import website.gradle.GrailsWebsiteExtension
@CompileStatic
@CacheableTask
class HtaccessTask extends GrailsWebsiteTask {
@Internal
- String description = 'Generates the .htaccess file'
+ final String description = 'Generates the .htaccess file'
public static final String NAME = 'genHtaccess'
@@ -54,7 +49,7 @@ class HtaccessTask extends GrailsWebsiteTask {
'https://www.gstatic.com/recaptcha/',
'https://hcaptcha.com',
'https://*.hcaptcha.com'
- ]
+ ]
private static String HT_ACCESS_CONTENT =
'# Custom 404 error page\n' +
@@ -71,11 +66,8 @@ class HtaccessTask extends GrailsWebsiteTask {
this.objects = objects
}
- @Internal
- final DirectoryProperty outputDir = objects.directoryProperty()
-
@OutputFile
- final RegularFileProperty htaccessFile = objects.fileProperty()
+ final RegularFileProperty htaccess = objects.fileProperty()
static TaskProvider<HtaccessTask> register(
Project project,
@@ -83,19 +75,12 @@ class HtaccessTask extends GrailsWebsiteTask {
String name = NAME
) {
project.tasks.register(name, HtaccessTask) {
- it.outputDir.set(siteExt.outputDir)
- it.htaccessFile.convention(
- project.layout.buildDirectory.file('dist/.htaccess')
- )
+ it.htaccess.set(siteExt.htaccess)
}
}
@TaskAction
void generateHtaccess() {
- def outputDir = new File(outputDir.get().asFile, 'dist').tap {mkdirs() }
- def htaccess = new File(outputDir, '.htaccess').tap {
- text = HT_ACCESS_CONTENT
- }
- logger.lifecycle('Generated .htaccess file at: {}', htaccess.absolutePath)
+ htaccess.get().asFile.text = HT_ACCESS_CONTENT
}
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/MinutesTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/MinutesTask.groovy
similarity index 91%
rename from buildSrc/src/main/groovy/org/grails/gradle/tasks/MinutesTask.groovy
rename to buildSrc/src/main/groovy/website/gradle/tasks/MinutesTask.groovy
index ef0652f0c1f..d401c5f564c 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/MinutesTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/MinutesTask.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import java.time.LocalDateTime
import java.time.ZoneId
@@ -49,20 +49,20 @@ import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.grails.utils.DateUtils
-import org.grails.HtmlMinutes
-import org.grails.MarkdownMinutes
-import org.grails.MinutesMetadataAdaptor
-import org.grails.documentation.SiteMap
-import org.grails.gradle.GrailsWebsiteExtension
-import org.grails.utils.MarkdownUtils
+import website.utils.DateUtils
+import website.model.HtmlMinutes
+import website.model.MarkdownMinutes
+import website.model.MinutesMetadataAdaptor
+import website.model.documentation.SiteMap
+import website.gradle.GrailsWebsiteExtension
+import website.utils.MarkdownUtils
@CompileStatic
@CacheableTask
class MinutesTask extends GrailsWebsiteTask {
@Internal
- String description =
+ final String description =
'Renders Markdown minutes (minutes/*.md) into HTML pages (dist/foundation/minutes/*.html). ' +
'It generates tag pages. Generates RSS feed. Minutes with future dates are not generated.'
@@ -155,14 +155,10 @@ class MinutesTask extends GrailsWebsiteTask {
outputDir.dir('foundation/minutes').get().asFile.tap { it.mkdirs() },
document.get().asFile.text
)
- copyBackgroundImages()
- }
-
- void copyBackgroundImages() {
project.copy { CopySpec copy ->
copy.from(assetsDir.dir('bgimages'))
copy.into(outputDir.dir('dist/images'))
- copy.include(CopyAssetsTask.IMAGE_EXTENSIONS)
+ copy.include(AssetsTask.assetTypes.images)
}
}
@@ -179,7 +175,7 @@ class MinutesTask extends GrailsWebsiteTask {
def writer = new StringWriter()
new MarkupBuilder(writer).with {
- div(class: 'headerbar chalicesbg') {
+ div(class: 'header-bar chalices-bg') {
div(class: 'content') {
h1 {
a(href: '[%url]/foundation/index.html', 'Foundation')
@@ -188,13 +184,13 @@ class MinutesTask extends GrailsWebsiteTask {
}
article(class: 'content container') {
- section(class: 'largegoldenratio align-left foundation-description') {
+ section(class: 'large-golden-ratio align-left foundation-description') {
div {
mkp.yieldUnescaped(htmlMinutes.html)
}
}
- section(class: 'smallgoldenratio align-left foundation-boards') {
+ section(class: 'small-golden-ratio align-left foundation-boards') {
div(class: 'meeting-archive-list') {
a(href: '[%url]/foundation/minutes/index.html', style: 'text-decoration: none', title: 'Meeting Minutes Archive') {
h2 { mkp.yield("Meeting Minutes Archive") }
@@ -221,10 +217,8 @@ class MinutesTask extends GrailsWebsiteTask {
def metadata = htmlMinutes.metadata.toMap()
html = RenderSiteTask.renderHtmlWithTemplateContent(html, metadata, templateText)
html = RenderSiteTask.highlightMenu(html, metadata, htmlMinutes.path)
- metadata['body'] = metadata['body'] ? metadata['body'] : 'foundation minutes'
- if (metadata['body']) {
- html = html.replace('<body>', "<body class='${metadata['body']}'>")
- }
+ metadata['bodyClassAttr'] = metadata['bodyClassAttr'] ?: 'foundation minutes'
+ html = html.replace('<body>', "<body class='${metadata['bodyClassAttr']}'>")
html
}
@@ -269,8 +263,7 @@ class MinutesTask extends GrailsWebsiteTask {
for (def htmlMinutes : listOfMinutes) {
minuteCards.add(minutesCard(htmlMinutes))
new File(outputDir, htmlMinutes.path).tap {
- it.createNewFile()
- it.text = renderMinutesHtml(htmlMinutes, templateText, listOfMinutes)
+ it.setText(renderMinutesHtml(htmlMinutes, templateText, listOfMinutes), 'UTF-8')
}
rssItems.add(
BlogTask.rssItemWithPage(
@@ -303,7 +296,7 @@ class MinutesTask extends GrailsWebsiteTask {
private static String minutesCard(HtmlMinutes htmlMinutes) {
def writer = new StringWriter()
new MarkupBuilder(writer).with {
- article(class: 'blogcard', style: "margin-bottom: 0; background-image: url($htmlMinutes.metadata.url/$IMAGES/$MINUTES_BG)") {
+ article(class: 'blog-card', style: "margin-bottom: 0; background-image: url($htmlMinutes.metadata.url/$IMAGES/$MINUTES_BG)") {
a(href: minutesLink(htmlMinutes)) {
h3 {
mkp.yield(RenderSiteTask.formatDate(htmlMinutes.metadata.date))
@@ -333,17 +326,14 @@ class MinutesTask extends GrailsWebsiteTask {
}
html = RenderSiteTask.renderHtmlWithTemplateContent(html, resolvedMetadata, templateText)
html = RenderSiteTask.highlightMenu(html, resolvedMetadata, "/$MINUTES/$INDEX")
- f.with {
- createNewFile()
- text = html
- }
+ f.setText(html, 'UTF-8')
}
@CompileDynamic
static String cardsHtml(List<String> cards, String title = null) {
def writer = new StringWriter()
new MarkupBuilder(writer).with {
- div(class: 'headerbar chalicesbg') {
+ div(class: 'header-bar chalices-bg') {
div(class: 'content') {
if (title) {
mkp.yieldUnescaped(title)
@@ -355,12 +345,12 @@ class MinutesTask extends GrailsWebsiteTask {
}
}
div(class: 'clear content container') {
- h3(class: 'columnheader', 'Meeting Minutes Archive')
+ h3(class: 'column-header', 'Meeting Minutes Archive')
div(class: 'light') {
div(class: 'padded', style: 'padding-top: 0;') {
for (int i = 0; i < cards.size(); i++) {
if (i == 0) {
- mkp.yieldUnescaped('<div class="threecolumns">')
+ mkp.yieldUnescaped('<div class="three-columns">')
}
div(class: 'column') {
mkp.yieldUnescaped(cards[i])
@@ -369,7 +359,7 @@ class MinutesTask extends GrailsWebsiteTask {
if (i != 0 && ((i + 1) % 3 == 0)) {
mkp.yieldUnescaped('</div>')
if (i != (cards.size() - 1)) {
- mkp.yieldUnescaped('<div class="threecolumns">')
+ mkp.yieldUnescaped('<div class="three-columns">')
}
}
}
diff --git a/buildSrc/src/main/groovy/website/gradle/tasks/PluginsTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/PluginsTask.groovy
new file mode 100644
index 00000000000..96b0cf19984
--- /dev/null
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/PluginsTask.groovy
@@ -0,0 +1,266 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package website.gradle.tasks
+
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+import javax.inject.Inject
+
+import groovy.json.JsonSlurper
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import groovy.util.logging.Slf4j
+
+import jakarta.annotation.Nullable
+
+import org.gradle.api.Project
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.ListProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.TaskProvider
+import website.gradle.GrailsWebsiteExtension
+import website.model.plugin.Owner
+import website.model.plugin.Plugin
+import website.model.plugin.PluginVersion
+import website.model.plugin.PluginsPage
+
+@Slf4j
+@CompileStatic
+@CacheableTask
+class PluginsTask extends GrailsWebsiteTask {
+
+ @Internal
+ final String description = 'Generates an HTML Page listing the Grails plugins'
+
+ public static final String NAME = 'genPlugins'
+
+ private static final String GRAILS_PLUGINS_JSON =
+ 'https://raw.githubusercontent.com/grails/grails-plugins-metadata/main/grails-plugins-index.json'
+
+ private final ObjectFactory objects
+
+ @Inject
+ PluginsTask(ObjectFactory objects) {
+ this.objects = objects
+ }
+
+ @InputFile
+ @PathSensitive(PathSensitivity.RELATIVE)
+ final RegularFileProperty document = objects.fileProperty()
+
+ @Input
+ final ListProperty<String> keywords = objects.listProperty(String)
+
+ @Input
+ final Property<String> url = objects.property(String)
+
+ @OutputDirectory
+ final DirectoryProperty outputDir = objects.directoryProperty()
+
+ static TaskProvider<PluginsTask> register(
+ Project project,
+ GrailsWebsiteExtension siteExt,
+ String name = NAME
+ ) {
+ project.tasks.register(name, PluginsTask) {
+ it.document.set(siteExt.template)
+ it.outputDir.set(siteExt.outputDir)
+ it.url.set(siteExt.url)
+ }
+ }
+
+ @TaskAction
+ void renderPluginsPage() {
+ def metadata = RenderSiteTask.siteMeta(
+ 'Grails Plugins', // TODO Make it configurable
+ 'List of Plugins', // TODO Make it configurable
+ url.get(),
+ keywords.get(),
+ 'all', // TODO Make it configurable,
+ '',
+ ''
+ )
+ metadata.put('JAVASCRIPT', '[%url]/javascripts/plugins-search.js')
+
+ def resolvedMetadata = RenderSiteTask.processMetadata(metadata)
+ def result = new JsonSlurper().parse(GRAILS_PLUGINS_JSON.toURL()) as List<Object>
+ def plugins = pluginsFromJson(result)
+ renderHtml(plugins, document.get().asFile.text, resolvedMetadata, 'plugins.html')
+ }
+
+ void renderHtml(List<Plugin> plugins, String templateText, Map<String, String> metadata, String fileName) {
+ def siteUrl = url.get()
+
+ def distDir = new File(outputDir.get().asFile, 'dist').tap { mkdirs() }
+ def pluginsDir = new File(distDir, 'plugins').tap { mkdirs() }
+ def pluginsTagsDir = new File(pluginsDir, 'tags').tap { mkdirs() }
+ def pluginsOwnersDir = new File(pluginsDir, 'owners').tap { mkdirs() }
+
+ def wrap = { String html -> RenderSiteTask.renderHtmlWithTemplateContent(html, metadata, templateText) }
+
+ // main plugins page
+ new File(distDir, fileName).setText(
+ RenderSiteTask.highlightMenu(
+ wrap(PluginsPage.mainContent(siteUrl, plugins, 'Grails Plugins', null)),
+ metadata,
+ '/plugins.html'
+ ),
+ 'UTF-8'
+ )
+
+ // tag pages
+ (plugins ?: [])
+ .collectMany { it.labels ?: ([] as List<String>) }
+ .unique()
+ .each { tag ->
+ new File(pluginsTagsDir, "${tag}.html").setText(
+ wrap(renderHtmlPagesForTags(siteUrl, plugins, tag)),
+ 'UTF-8'
+ )
+ }
+
+ // owner pages
+ (plugins ?: [])
+ .collect { it.owner?.name }
+ .findAll { it }
+ .unique()
+ .each { owner ->
+ new File(pluginsOwnersDir, "${owner}.html").setText(
+ wrap(renderHtmlPagesForOwners(siteUrl, plugins, owner)),
+ 'UTF-8'
+ )
+ }
+ }
+
+ static String renderHtmlPagesForTags(String siteUrl, List<Plugin> plugins, String tag) {
+ def filteredPlugins = (plugins ?: []).findAll { (it.labels ?: []).contains(tag) }
+ PluginsPage.mainContent(siteUrl, plugins, "Plugins by tag #$tag", filteredPlugins)
+ }
+
+ static String renderHtmlPagesForOwners(String siteUrl, List<Plugin> plugins, String owner) {
+ def filteredPlugins = (plugins ?: []).findAll { it.owner?.name == owner }
+ PluginsPage.mainContent(siteUrl, plugins, "Plugins by creator: #$owner", filteredPlugins)
+ }
+
+ /**
+ * Converts the JSON array from grails-plugins-index.json into a list of {@link Plugin} objects.
+ * Extracts the latest version and update date from the first entry in each plugin's versions array.
+ *
+ * @param json the parsed JSON array from the plugins index
+ * @return list of Plugin objects
+ */
+ @CompileDynamic
+ static List<Plugin> pluginsFromJson(List json) {
+ (json ?: []).collect { item ->
+ def versionsJson = item['versions'] as List
+ def latestVersionEntry = versionsJson?.first()
+
+ // Parse all versions
+ def versions = (versionsJson ?: []).collect { v ->
+ new PluginVersion(
+ version: v['version'],
+ date: v['date'] ? parseIsoStringToDate(v['date'] as String) : null,
+ grailsVersion: v['grailsVersion']
+ )
+ } as List<PluginVersion>
+
+ new Plugin(
+ name: item['name'],
+ desc: item['desc'],
+ coords: item['coords'],
+ vcsUrl: item['vcs'],
+ docsUrl: item['docs'],
+ mavenRepo: item['maven-repo'],
+ owner: new Owner(name: item['owner']),
+ latestVersion: latestVersionEntry?.version,
+ grailsVersion: latestVersionEntry?.grailsVersion,
+ updated: latestVersionEntry?.date ? parseIsoStringToDate(latestVersionEntry.date as String) : null,
+ githubStars: githubStars(item['vcs'] as String),
+ labels: (item['labels'] ?: []) as List<String>,
+ license: (item['licenses'] as List)?.first(),
+ deprecated: item['deprecated'] as String,
+ versions: versions
+ )
+ }
+ }
+
+ /**
+ * Parses an ISO 8601 formatted date string into a {@link LocalDateTime}.
+ *
+ * @param isoFormattedString the date string (e.g., "2025-12-19T15:35:33+0000")
+ * @return parsed LocalDateTime
+ */
+ static LocalDateTime parseIsoStringToDate(String isoFormattedString) {
+ def f = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ")
+ LocalDateTime.parse(isoFormattedString, f)
+ }
+
+ /**
+ * Fetches the GitHub star count for a repository using the GitHub API.
+ * Requires the GH_TOKEN environment variable to be set.
+ *
+ * @param vcsUrl the GitHub repository URL
+ * @return the number of stars, or {@code null} if unavailable or not a GitHub repo
+ */
+ @Nullable
+ @CompileDynamic
+ static Integer githubStars(String vcsUrl) {
+ def token = System.getenv('GH_TOKEN')
+ if (!vcsUrl || !vcsUrl.contains('github.com') || !token) {
+ return null
+ }
+ try {
+ log.info('Fetching github stars of {}', vcsUrl)
+ def repoPath = vcsUrl.replaceFirst(/^.*github\.com\//, '')
+ def url = "https://api.github.com/repos/$repoPath"
+ def request = HttpRequest.newBuilder()
+ .uri(new URI(url))
+ .header('X-GitHub-Api-Version', '2022-11-28')
+ .header('Authorization', "Bearer $token")
+ .GET()
+ .build()
+ def response = HttpClient.newHttpClient()
+ .send(request, HttpResponse.BodyHandlers.ofString())
+
+ if (response.statusCode() != 200) {
+ return null
+ }
+ def stars = new JsonSlurper()
+ .parseText(response.body())['stargazers_count'] as Integer
+ return stars ?: null // 0 → null
+ } catch (Exception e) {
+ log.error("Error fetching GitHub stars for $vcsUrl", e)
+ return null
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/ProfilesTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/ProfilesTask.groovy
similarity index 79%
rename from buildSrc/src/main/groovy/org/grails/gradle/tasks/ProfilesTask.groovy
rename to buildSrc/src/main/groovy/website/gradle/tasks/ProfilesTask.groovy
index 60d1bc01f97..92a1102a1de 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/ProfilesTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/ProfilesTask.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import javax.inject.Inject
@@ -35,15 +35,15 @@ import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.grails.documentation.ProfilesPage
-import org.grails.gradle.GrailsWebsiteExtension
+import website.model.documentation.ProfilesPage
+import website.gradle.GrailsWebsiteExtension
@CompileStatic
@CacheableTask
class ProfilesTask extends GrailsWebsiteTask {
@Internal
- String description = 'Generates the Profiles HTML Page -> build/temp/profiles.html'
+ final String description = 'Generates the Profiles HTML Page -> build/temp/profiles.html'
public static final String NAME = 'genProfilesPage'
@@ -69,14 +69,15 @@ class ProfilesTask extends GrailsWebsiteTask {
}
@TaskAction
- void renderDocsPage() {
- def buildDir = outputDir.get().asFile
- def tempDir = new File(buildDir, 'temp').tap { mkdir() }
- def output = new File(tempDir, 'profiles').tap { createNewFile() }
- output.text =
+ void renderProfilesPage() {
+ def tempDir = new File(outputDir.get().asFile, 'temp').tap { mkdirs() }
+ def output = new File(tempDir, 'profiles.html')
+ output.setText(
'title: Profiles | Grails Framework\n' +
- 'body: docs\n' +
- '---\n' +
- ProfilesPage.mainContent(profiles.get().asFile)
+ 'body: docs\n' +
+ '---\n' +
+ ProfilesPage.mainContent(profiles.get().asFile),
+ 'UTF-8'
+ )
}
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/QuestionsTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/QuestionsTask.groovy
similarity index 84%
rename from buildSrc/src/main/groovy/org/grails/gradle/tasks/QuestionsTask.groovy
rename to buildSrc/src/main/groovy/website/gradle/tasks/QuestionsTask.groovy
index 18c4be1fb8e..8ac574545ac 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/QuestionsTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/QuestionsTask.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import javax.inject.Inject
@@ -35,15 +35,15 @@ import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.grails.gradle.GrailsWebsiteExtension
-import org.grails.questions.QuestionsPage
+import website.gradle.GrailsWebsiteExtension
+import website.model.questions.QuestionsPage
@CompileStatic
@CacheableTask
class QuestionsTask extends GrailsWebsiteTask {
@Internal
- String description = 'Generates FAQ HTML - build/temp/faq.html'
+ final String description = 'Generates FAQ HTML - build/temp/faq.html'
public static final String NAME = 'genFaq'
@@ -77,11 +77,11 @@ class QuestionsTask extends GrailsWebsiteTask {
@TaskAction
void renderQuestionsPage() {
def buildDir = outputDir.get().asFile
- def temp = new File(buildDir, 'temp').tap { it.mkdir() }
-
+ def temp = new File(buildDir, 'temp').tap { it.mkdirs() }
def output = new File(temp, PAGE_NAME_QUESTIONS)
- output.createNewFile()
- output.text = "title: FAQ | Apache Grails®\nbody: faq\n---\n" +
- QuestionsPage.mainContent(questions.get().asFile)
+ output.setText(
+ "title: FAQ | Apache Grails®\nbody: faq\n---\n" + QuestionsPage.mainContent(questions.get().asFile),
+ 'UTF-8'
+ )
}
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/RenderSiteTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/RenderSiteTask.groovy
similarity index 94%
rename from buildSrc/src/main/groovy/org/grails/gradle/tasks/RenderSiteTask.groovy
rename to buildSrc/src/main/groovy/website/gradle/tasks/RenderSiteTask.groovy
index c9ff5bd01db..7000abe4534 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/RenderSiteTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/RenderSiteTask.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import javax.annotation.Nonnull
import javax.inject.Inject
@@ -44,11 +44,11 @@ import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.grails.ContentAndMetadata
-import org.grails.utils.DateUtils
-import org.grails.Page
-import org.grails.documentation.SiteMap
-import org.grails.gradle.GrailsWebsiteExtension
+import website.model.ContentAndMetadata
+import website.utils.DateUtils
+import website.model.Page
+import website.model.documentation.SiteMap
+import website.gradle.GrailsWebsiteExtension
import static groovy.io.FileType.FILES
@@ -57,7 +57,7 @@ import static groovy.io.FileType.FILES
class RenderSiteTask extends GrailsWebsiteTask {
@Internal
- String description =
+ final String description =
'Build Grails website - generates pages with HTML entries in pages and build/temp, ' +
'renders blog and RSS feed, copies assets and generates a sitemap'
@@ -133,7 +133,7 @@ class RenderSiteTask extends GrailsWebsiteTask {
.reverse()
.collect { "<option>${it}</option>" }
.join(' ')
- def m = siteMeta(
+ def m = website.gradle.tasks.RenderSiteTask.siteMeta(
title.get(),
about.get(),
url.get(),
@@ -175,31 +175,30 @@ class RenderSiteTask extends GrailsWebsiteTask {
static void renderPages(
Map<String,
- String> sitemeta,
+ String> siteMeta,
List<Page> listOfPages,
File outputDir,
String templateText
) {
- for (Page page : listOfPages) {
+ for (def page : listOfPages) {
def resolvedMetadata = processMetadata(
- sitemeta + page.metadata
+ siteMeta + page.metadata
)
def html = renderHtmlWithTemplateContent(
page.content,
resolvedMetadata,
templateText
)
- html = highlightMenu(html, sitemeta, page.path)
- if (page.body) {
- html = html.replace('<body>', "<body class='${page.body}'>")
+ html = highlightMenu(html, siteMeta, page.path)
+ if (page.bodyClassAttr) {
+ html = html.replace('<body>', "<body class='${page.bodyClassAttr}'>")
}
saveHtmlToPath(outputDir, html, page.path)
}
}
static void saveHtmlToPath(File outputDir, String html, String filepath) {
- def pageOutput = new File(outputDir.absolutePath)
- pageOutput.mkdir()
+ def pageOutput = new File(outputDir.absolutePath).tap { it.mkdirs() }
def paths = filepath.split('/')
for (String path : paths) {
if (path.endsWith('.html')) {
@@ -207,16 +206,14 @@ class RenderSiteTask extends GrailsWebsiteTask {
} else if (path.trim().isEmpty()) {
// continue
} else {
- pageOutput = new File(pageOutput, path)
- pageOutput.mkdir()
+ pageOutput = new File(pageOutput, path).tap { it.mkdirs() }
}
}
- pageOutput.createNewFile()
- pageOutput.text = html
+ pageOutput.setText(html, 'UTF-8')
}
- static Map<String, String> processMetadata(Map<String, String> sitemeta) {
- def resolvedMetadata = sitemeta
+ static Map<String, String> processMetadata(Map<String, String> siteMeta) {
+ def resolvedMetadata = siteMeta
if (resolvedMetadata.containsKey('CSS')) {
resolvedMetadata.put(
'CSS',
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/SitemapTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/SitemapTask.groovy
similarity index 92%
rename from buildSrc/src/main/groovy/org/grails/gradle/tasks/SitemapTask.groovy
rename to buildSrc/src/main/groovy/website/gradle/tasks/SitemapTask.groovy
index cc855329cd4..5cb8d978b7a 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/SitemapTask.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/SitemapTask.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.gradle.tasks
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
@@ -39,7 +39,7 @@ import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
-import org.grails.gradle.GrailsWebsiteExtension
+import website.gradle.GrailsWebsiteExtension
import static groovy.io.FileType.FILES
@@ -48,7 +48,7 @@ import static groovy.io.FileType.FILES
class SitemapTask extends GrailsWebsiteTask {
@Internal
- String description = 'Generates build/dist/sitemap.xml with every page in the site'
+ final String description = 'Generates build/dist/sitemap.xml with every page in the site'
public static final String NAME = 'genSitemap'
@@ -100,8 +100,8 @@ class SitemapTask extends GrailsWebsiteTask {
@CompileDynamic
static String sitemapContent(List<String> urls) {
def writer = new StringWriter()
- def html = new MarkupBuilder(writer)
- html.urlset(xmlns: 'https://www.sitemaps.org/schemas/sitemap/0.9') {
+ def xml = new MarkupBuilder(writer)
+ xml.urlset(xmlns: 'https://www.sitemaps.org/schemas/sitemap/0.9') {
for (def urlStr : urls) {
url {
loc(urlStr)
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy b/buildSrc/src/main/groovy/website/model/Content.groovy
similarity index 67%
copy from buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
copy to buildSrc/src/main/groovy/website/model/Content.groovy
index 6c7fa74fa36..a5866f82914 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
+++ b/buildSrc/src/main/groovy/website/model/Content.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,11 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.model
-import org.gradle.api.DefaultTask
+import jakarta.annotation.Nullable
-abstract class GrailsWebsiteTask extends DefaultTask {
+interface Content {
+
+ @Nullable String getPath();
+ @Nullable String getTitle();
+ @Nullable String getBodyClassAttr();
+ @Nullable String getDate();
+ @Nullable String getDescription();
+ @Nullable List<String> getKeywords();
+ @Nullable String getRobots();
- String group = 'grails website'
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy b/buildSrc/src/main/groovy/website/model/ContentAndMetadata.groovy
similarity index 79%
copy from buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
copy to buildSrc/src/main/groovy/website/model/ContentAndMetadata.groovy
index 6c7fa74fa36..9c958a1d0f4 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
+++ b/buildSrc/src/main/groovy/website/model/ContentAndMetadata.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,11 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
-import org.gradle.api.DefaultTask
+package website.model
-abstract class GrailsWebsiteTask extends DefaultTask {
+import groovy.transform.CompileStatic
- String group = 'grails website'
+@CompileStatic
+class ContentAndMetadata {
+
+ String content
+
+ Map<String, String> metadata
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy b/buildSrc/src/main/groovy/website/model/HtmlMinutes.groovy
similarity index 79%
copy from buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
copy to buildSrc/src/main/groovy/website/model/HtmlMinutes.groovy
index 6c7fa74fa36..efa8c871186 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
+++ b/buildSrc/src/main/groovy/website/model/HtmlMinutes.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,11 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.model
-import org.gradle.api.DefaultTask
+class HtmlMinutes {
-abstract class GrailsWebsiteTask extends DefaultTask {
+ String html
+ String path
- String group = 'grails website'
+ MinutesMetadata metadata
}
diff --git a/buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy b/buildSrc/src/main/groovy/website/model/HtmlPost.groovy
similarity index 78%
copy from buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy
copy to buildSrc/src/main/groovy/website/model/HtmlPost.groovy
index 455b609980e..f481bbc4a4c 100644
--- a/buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy
+++ b/buildSrc/src/main/groovy/website/model/HtmlPost.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,15 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails
+package website.model
import groovy.transform.CompileStatic
@CompileStatic
-class MarkdownPost extends Page {
+class HtmlPost {
- @Override
- String getPath() {
- return filename.replace('.md', '.html').replace('.markdown', '.html')
- }
+ String html
+ String path
+
+ PostMetadata metadata
+
+ Set<String> tags
}
diff --git a/buildSrc/src/main/groovy/org/grails/MarkdownMinutes.groovy b/buildSrc/src/main/groovy/website/model/MarkdownMinutes.groovy
similarity index 97%
copy from buildSrc/src/main/groovy/org/grails/MarkdownMinutes.groovy
copy to buildSrc/src/main/groovy/website/model/MarkdownMinutes.groovy
index e1254250eb3..090a2b8198a 100644
--- a/buildSrc/src/main/groovy/org/grails/MarkdownMinutes.groovy
+++ b/buildSrc/src/main/groovy/website/model/MarkdownMinutes.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails
+package website.model
import groovy.transform.CompileStatic
diff --git a/buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy b/buildSrc/src/main/groovy/website/model/MarkdownPost.groovy
similarity index 90%
copy from buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy
copy to buildSrc/src/main/groovy/website/model/MarkdownPost.groovy
index 455b609980e..29ba2830d3c 100644
--- a/buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy
+++ b/buildSrc/src/main/groovy/website/model/MarkdownPost.groovy
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails
+package website.model
import groovy.transform.CompileStatic
@@ -25,6 +25,6 @@ class MarkdownPost extends Page {
@Override
String getPath() {
- return filename.replace('.md', '.html').replace('.markdown', '.html')
+ filename.replace('.md', '.html').replace('.markdown', '.html')
}
}
diff --git a/buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy b/buildSrc/src/main/groovy/website/model/MinutesMetadata.groovy
similarity index 72%
copy from buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy
copy to buildSrc/src/main/groovy/website/model/MinutesMetadata.groovy
index 455b609980e..3e006f960d6 100644
--- a/buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy
+++ b/buildSrc/src/main/groovy/website/model/MinutesMetadata.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,15 +16,25 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails
+package website.model
+import edu.umd.cs.findbugs.annotations.Nullable
import groovy.transform.CompileStatic
@CompileStatic
-class MarkdownPost extends Page {
+interface MinutesMetadata {
+
+ String get(String name)
+
+ @Nullable
+ String getUrl()
+
+ @Nullable
+ String getTitle()
+
+ @Nullable
+ String getDate()
+
+ Map<String, String> toMap()
- @Override
- String getPath() {
- return filename.replace('.md', '.html').replace('.markdown', '.html')
- }
}
diff --git a/buildSrc/src/main/groovy/org/grails/guides/GrailsMajorVersion.groovy b/buildSrc/src/main/groovy/website/model/MinutesMetadataAdaptor.groovy
similarity index 59%
copy from buildSrc/src/main/groovy/org/grails/guides/GrailsMajorVersion.groovy
copy to buildSrc/src/main/groovy/website/model/MinutesMetadataAdaptor.groovy
index 75ac7247430..4969b23a447 100644
--- a/buildSrc/src/main/groovy/org/grails/guides/GrailsMajorVersion.groovy
+++ b/buildSrc/src/main/groovy/website/model/MinutesMetadataAdaptor.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,27 +16,42 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.guides
+package website.model
import groovy.transform.CompileStatic
@CompileStatic
-enum GrailsMajorVersion {
+class MinutesMetadataAdaptor implements MinutesMetadata {
- GRAILS_3(3),
- GRAILS_4(4),
- GRAILS_5(5),
- GRAILS_6(6),
- GRAILS_7(7)
+ Map<String, String> metadata
- final int id
+ MinutesMetadataAdaptor(Map<String, String> metadata) {
+ this.metadata = metadata
- private GrailsMajorVersion(int id) {
- this.id = id
}
@Override
- String toString() {
- this.name()
+ String get(String name) {
+ return metadata[name]
}
-}
\ No newline at end of file
+
+ @Override
+ String getUrl() {
+ get('url')
+ }
+
+ @Override
+ String getTitle() {
+ get('title')
+ }
+
+ @Override
+ String getDate() {
+ get('date')
+ }
+
+ @Override
+ Map<String, String> toMap() {
+ metadata
+ }
+}
diff --git a/buildSrc/src/main/groovy/org/grails/Page.groovy b/buildSrc/src/main/groovy/website/model/Page.groovy
similarity index 93%
rename from buildSrc/src/main/groovy/org/grails/Page.groovy
rename to buildSrc/src/main/groovy/website/model/Page.groovy
index 83068c289f2..bff0ecf4bd4 100644
--- a/buildSrc/src/main/groovy/org/grails/Page.groovy
+++ b/buildSrc/src/main/groovy/website/model/Page.groovy
@@ -16,17 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails
+package website.model
import groovy.transform.CompileStatic
-import org.grails.utils.DateUtils
+import website.utils.DateUtils
@CompileStatic
class Page implements Content, Comparable<Page> {
String filename
String content
+
Map<String, String> metadata
@Override
@@ -40,8 +41,8 @@ class Page implements Content, Comparable<Page> {
}
@Override
- String getBody() {
- metadata?.get('body')
+ String getBodyClassAttr() {
+ metadata?.get('bodyClassAttr')
}
@Override
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy b/buildSrc/src/main/groovy/website/model/PageElement.groovy
similarity index 79%
copy from buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
copy to buildSrc/src/main/groovy/website/model/PageElement.groovy
index 6c7fa74fa36..539da9d6fa9 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
+++ b/buildSrc/src/main/groovy/website/model/PageElement.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,11 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.model
-import org.gradle.api.DefaultTask
+import groovy.transform.CompileStatic
-abstract class GrailsWebsiteTask extends DefaultTask {
+@CompileStatic
+interface PageElement {
- String group = 'grails website'
+ String renderAsHtml()
}
diff --git a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy b/buildSrc/src/main/groovy/website/model/PostMetadata.groovy
similarity index 69%
rename from buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
rename to buildSrc/src/main/groovy/website/model/PostMetadata.groovy
index 6c7fa74fa36..8c99daff896 100644
--- a/buildSrc/src/main/groovy/org/grails/gradle/tasks/GrailsWebsiteTask.groovy
+++ b/buildSrc/src/main/groovy/website/model/PostMetadata.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,11 +16,25 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.gradle.tasks
+package website.model
-import org.gradle.api.DefaultTask
+import jakarta.annotation.Nullable
-abstract class GrailsWebsiteTask extends DefaultTask {
+interface PostMetadata {
- String group = 'grails website'
-}
+ String get(String name)
+
+ @Nullable
+ String getUrl()
+
+ @Nullable
+ String getTitle()
+
+ @Nullable
+ String getAuthor()
+
+ @Nullable
+ String getDate()
+
+ Map<String, String> toMap()
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/org/grails/guides/GrailsMajorVersion.groovy b/buildSrc/src/main/groovy/website/model/PostMetadataAdapter.groovy
similarity index 57%
copy from buildSrc/src/main/groovy/org/grails/guides/GrailsMajorVersion.groovy
copy to buildSrc/src/main/groovy/website/model/PostMetadataAdapter.groovy
index 75ac7247430..ce7a5035314 100644
--- a/buildSrc/src/main/groovy/org/grails/guides/GrailsMajorVersion.groovy
+++ b/buildSrc/src/main/groovy/website/model/PostMetadataAdapter.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,27 +16,47 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails.guides
+package website.model
import groovy.transform.CompileStatic
@CompileStatic
-enum GrailsMajorVersion {
+class PostMetadataAdapter implements PostMetadata {
- GRAILS_3(3),
- GRAILS_4(4),
- GRAILS_5(5),
- GRAILS_6(6),
- GRAILS_7(7)
+ Map<String, String> metadata
- final int id
+ PostMetadataAdapter(Map<String, String> metadata) {
+ this.metadata = metadata
- private GrailsMajorVersion(int id) {
- this.id = id
}
@Override
- String toString() {
- this.name()
+ String get(String name) {
+ return metadata[name]
}
-}
\ No newline at end of file
+
+ @Override
+ String getUrl() {
+ get('url')
+ }
+
+ @Override
+ String getTitle() {
+ get('title')
+ }
+
+ @Override
+ String getAuthor() {
+ get('author')
+ }
+
+ @Override
+ String getDate() {
+ get('date')
+ }
+
+ @Override
+ Map<String, String> toMap() {
+ metadata
+ }
+}
diff --git a/buildSrc/src/main/groovy/website/model/documentation/DocumentationPage.groovy b/buildSrc/src/main/groovy/website/model/documentation/DocumentationPage.groovy
new file mode 100644
index 00000000000..33fa011bfca
--- /dev/null
+++ b/buildSrc/src/main/groovy/website/model/documentation/DocumentationPage.groovy
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package website.model.documentation
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+import org.yaml.snakeyaml.Yaml
+
+import static website.utils.RenderUtils.renderHtml
+
+@CompileStatic
+class DocumentationPage {
+
+ /**
+ * Renders a single documentation category as an HTML guide group.
+ * Displays the category's icon, title, and a list of linked items.
+ *
+ * @param category the documentation category to render
+ * @return HTML string representing the category section
+ */
+ @CompileDynamic
+ private static String renderCategory(DocumentationCategory category) {
+ renderHtml {
+ div(class: 'guide-group') {
+ div(class: 'guide-group-header') {
+ img(src: category.image, alt: category.title)
+ h2(category.title)
+ }
+ ul {
+ category.items.each { item ->
+ li {
+ a(href: item.url, item.title)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Renders the documentation links section for a specific Grails version.
+ * Includes links to the User Guide and API Reference.
+ *
+ * @param version the Grails version string (e.g., "6.2.0", "snapshot")
+ * @return HTML string with documentation links for the specified version
+ */
+ @CompileDynamic
+ static String renderDocumentation(String version) {
+ renderHtml {
+ div(class: 'guide-group') {
+ if (version) {
+ div(class: 'guide-group-header') {
+ img(
+ src: '[%url]/images/documentation.svg',
+ alt: "Grails Version ($version)"
+ )
+ h2(
+ DocumentationPage.resolveDocumentationName(version)
+ )
+ }
+ ul {
+ li {
+ a(
+ href: "https://grails.apache.org/docs/$version/",
+ 'User Guide'
+ )
+ }
+ li {
+ a(
+ href: "https://grails.apache.org/docs/$version/api/",
+ 'API Reference'
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Resolves a human-readable documentation title based on the version string.
+ * Determines whether the version is a Snapshot, Milestone, Release Candidate, or Latest release.
+ *
+ * @param version the Grails version string
+ * @return formatted documentation title (e.g., "Latest Version (6.2.0) Documentation")
+ */
+ static String resolveDocumentationName(String version) {
+ def v = version.toLowerCase()
+ def label =
+ v.endsWith('-snapshot') || v.contains('snapshot') ? 'Snapshot' :
+ version.contains('.M') ? 'Milestone' :
+ version.contains('.RC') ? 'Release Candidate' :
+ 'Latest'
+
+ "$label Version ($version) Documentation"
+ }
+
+ /**
+ * Generates the main documentation page HTML content.
+ * Displays pre-release, snapshot, and latest version documentation links,
+ * along with categorized documentation for GORM, Security, and other modules.
+ * Also includes version selectors for browsing older documentation.
+ *
+ * @param releases the YAML file containing Grails release information
+ * @param modules the YAML file containing module/category definitions
+ * @return complete HTML string for the documentation page
+ */
+ @CompileDynamic
+ static String mainContent(File releases, File modules) {
+ renderHtml {
+ div(class: 'header-bar chalices-bg') {
+ div(class: 'content') {
+ h1('Documentation')
+ }
+ }
+ def preRelease = SiteMap.latestPreReleaseVersion(releases)
+ def latest = SiteMap.latestVersion(releases)
+ def categories = DocumentationPage.categories(modules)
+ div(class: 'content') {
+ div(class: 'two-columns') {
+ div(class: 'odd column'){
+ if (preRelease > latest) {
+ mkp.yieldUnescaped(
+ DocumentationPage.renderDocumentation(preRelease.versionText)
+ )
+ }
+ mkp.yieldUnescaped(
+ DocumentationPage.renderDocumentation('snapshot')
+ )
+ mkp.yieldUnescaped(
+ DocumentationPage.renderCategory(categories.find {
+ it.title == 'GORM - Data Access Toolkit'
+ })
+ )
+ mkp.yieldUnescaped(
+ DocumentationPage.renderCategory(categories.find {
+ it.title == 'Security'
+ })
+ )
+
+ }
+ div(class: 'column') {
+ mkp.yieldUnescaped(
+ DocumentationPage.renderDocumentation(latest.versionText)
+ )
+ div(class: 'older-versions') {
+ h3(
+ class: 'column-header',
+ style: 'margin-bottom: 10px',
+ 'Older Version'
+ )
+ p('Browse previous versions\' documentation since Grails 1.2.0')
+ div(class: 'version-selector') {
+ h4('Single Page - User Guide')
+ select(onchange: "window.location.href='https://grails.apache.org/docs/' + this.value + '/guide/single.html'") {
+ option('Select a version')
+ mkp.yield('[%versions]')
+ }
+ }
+ div(class: 'version-selector') {
+ h4('User Guide')
+ select(onchange: "window.location.href='https://grails.apache.org/docs/' + this.value") {
+ option('Select a version')
+ mkp.yield('[%versions]')
+ }
+ }
+ div(class: 'version-selector') {
+ h4('API Reference')
+ select(onchange: "window.location.href='https://grails.apache.org/docs/' + this.value + '/api'") {
+ option('Select a version')
+ mkp.yield('[%versions]')
+ }
+ }
+ }
+ ['Upgrade', 'Testing', 'Views', 'Async', 'Database', 'Redis'].each { title ->
+ mkp.yieldUnescaped(
+ DocumentationPage.renderCategory(categories.find {
+ it.title == title
+ })
+ )
+ }
+ }
+
+ }
+ }
+ }
+ }
+
+ /**
+ * Parses the modules YAML file and groups documentation items by category.
+ * Each category contains metadata (title, image, description, URL) and a list of items.
+ *
+ * @param modules the YAML file containing module definitions
+ * @return collection of {@link DocumentationCategory} objects with their items populated
+ */
+ @CompileDynamic
+ private static Collection<DocumentationCategory> categories(File modules) {
+ def model = modules.newInputStream().withCloseable { is ->
+ new Yaml().load(is) as Map
+ }
+ Map<String, DocumentationCategory> byCategory = [:].withDefault { key ->
+ new DocumentationCategory(title: key)
+ }
+ (model.modules as Map).each { k, v ->
+ def cat = byCategory[v['category']]
+ cat.with {
+ image = v['categoryImage'] ?: null
+ description = v['categoryDescription'] ?: null
+ url = v['categoryUrl'] ?: null
+ }
+ cat.items << new DocumentationItem(url: v['url'], title: v['title'])
+ }
+ byCategory.values()
+ }
+
+ /**
+ * Represents a documentation category containing related documentation items.
+ * Categories group items like "GORM", "Security", "Testing", etc.
+ */
+ @CompileStatic
+ static class DocumentationCategory {
+ String description
+ String image
+ String title
+ String url
+ List<DocumentationItem> items = []
+ }
+
+ /**
+ * Represents a single documentation item within a category.
+ * Contains a title and URL linking to the documentation resource.
+ */
+ @CompileStatic
+ static class DocumentationItem {
+ String title
+ String url
+ }
+}
diff --git a/buildSrc/src/main/groovy/website/model/documentation/DownloadPage.groovy b/buildSrc/src/main/groovy/website/model/documentation/DownloadPage.groovy
new file mode 100644
index 00000000000..6f13d25d41a
--- /dev/null
+++ b/buildSrc/src/main/groovy/website/model/documentation/DownloadPage.groovy
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package website.model.documentation
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+
+import static website.utils.RenderUtils.renderHtml
+
+@CompileStatic
+class DownloadPage {
+
+ static String binaryUrl(String version, String artifact = 'grails', String ext = '', String directory = 'core') {
+ "https://www.apache.org/dyn/closer.lua/grails/$directory/$version/distribution/apache-$artifact-$version-bin.zip$ext?action=download"
+ }
+
+ static String sourceUrl(String version, String artifact = 'grails', String ext = '', String directory = 'core') {
+ "https://www.apache.org/dyn/closer.lua/grails/$directory/$version/sources/apache-$artifact-$version-src.zip$ext?action=download"
+ }
+
+ static String binaryVerificationUrl(String version, String artifact = 'grails', String ext = '', String directory = 'core') {
+ "https://downloads.apache.org/grails/$directory/$version/distribution/apache-$artifact-$version-bin.zip$ext"
+ }
+
+ static String sourceVerificationUrl(String version, String artifact = 'grails', String ext = '', String directory = 'core') {
+ "https://downloads.apache.org/grails/$directory/$version/sources/apache-$artifact-$version-src.zip$ext"
+ }
+
+ /**
+ * Does not handle pre-release versions as these are not displayed in the select box.
+ */
+ static String resolveOldDownloadUrl(String version) {
+ def parts = ((version.split(/\./)*.replaceAll(/\D.*/, '')*.toInteger()) + [0, 0, 0]).take(3)
+ def (major, minor, patch) = [parts[0], parts[1], parts[2]]
+ def tag = "v$version"
+ if (major < 7) {
+ def baseUrl = 'https://github.com/apache/grails-core/releases/download'
+ def artifactName = "grails-$version"
+ if (major == 6) {
+ baseUrl = 'https://github.com/apache/grails-forge/releases/download'
+ artifactName = "grails-cli-$version"
+ }
+ if (major == 1 && minor == 1) {
+ artifactName = "grails-bin-$version"
+ }
+ if (major <= 1 && patch == 0) {
+ tag = "v$major.$minor"
+ }
+ return "$baseUrl/$tag/${artifactName}.zip"
+ }
+ else {
+ return "https://www.apache.org/dyn/closer.lua/grails/core/$version/distribution/apache-grails-$version-bin.zip?action=download"
+ }
+ }
+
+ @CompileDynamic
+ static String renderDownload(String version) {
+
+ if (version.toLowerCase().contains('snapshot')) {
+ return ''
+ }
+
+ def redisVersion = '5.0.0'
+ def quartzVersion = '4.0.0'
+ def springSecurityVersion = '7.0.0'
+ def grailsGithubActionsVersion = '1.0.1'
+ def grailsGradlePublishVersion = '0.0.4'
+
+ renderHtml {
+ div(class: 'guide-group') {
+ if (version) {
+ div(class: 'guide-group-header') {
+ img(src: '[%url]/images/download.svg', alt: "Download Grails ($version)")
+ h2(
+ DocumentationPage.resolveDocumentationName(version)
+ )
+ }
+ ul {
+ if (version.startsWith('7')) {
+ li {
+ a(href: DownloadPage.sourceUrl(version), 'Source')
+ a(href: sourceVerificationUrl(version, 'grails', '.sha512'), 'SHA512')
+ a(href: sourceVerificationUrl(version, 'grails', '.asc'), 'ASC')
+ }
+ li {
+ a(href: binaryUrl(version, 'grails'), 'Binary')
+ a(href: binaryVerificationUrl(version, 'grails', '.sha512'), 'SHA512')
+ a(href: binaryVerificationUrl(version, 'grails', '.asc'), 'ASC')
+ }
+ li {
+ a(href: binaryUrl(version, 'grails-wrapper'), 'Binary Wrapper')
+ a(href: binaryVerificationUrl(version, 'grails-wrapper', '.sha512'), 'SHA512')
+ a(href: binaryVerificationUrl(version, 'grails-wrapper', '.asc'), 'ASC')
+ }
+ li {
+ a(href: sourceUrl(springSecurityVersion, 'grails-spring-security', '', 'spring-security'), "Grails Spring Security $springSecurityVersion Plugin Source")
+ a(href: sourceVerificationUrl(springSecurityVersion, 'grails-spring-security', '.sha512', 'spring-security'), 'SHA512')
+ a(href: sourceVerificationUrl(springSecurityVersion, 'grails-spring-security', '.asc', 'spring-security'), 'ASC')
+ }
+ li {
+ a(href: sourceUrl(redisVersion, 'grails-redis', '', 'redis'), "Grails Redis $redisVersion Plugin Source")
+ a(href: sourceVerificationUrl(redisVersion, 'grails-redis', '.sha512', 'redis'), 'SHA512')
+ a(href: sourceVerificationUrl(redisVersion, 'grails-redis', '.asc', 'redis'), 'ASC')
+ }
+ li {
+ a(href: sourceUrl(quartzVersion, 'grails-quartz', '', 'quartz'), "Grails Quartz $quartzVersion Plugin Source")
+ a(href: sourceVerificationUrl(quartzVersion, 'grails-quartz', '.sha512', 'quartz'), 'SHA512')
+ a(href: sourceVerificationUrl(quartzVersion, 'grails-quartz', '.asc', 'quartz'), 'ASC')
+ }
+ li {
+ a(href: sourceUrl(grailsGithubActionsVersion, 'grails-github-actions', '', 'actions'), "Grails GitHub Actions $grailsGithubActionsVersion Source")
+ a(href: sourceVerificationUrl(grailsGithubActionsVersion, 'grails-github-actions', '.sha512', 'actions'), 'SHA512')
+ a(href: sourceVerificationUrl(grailsGithubActionsVersion, 'grails-github-actions', '.asc', 'actions'), 'ASC')
+ }
+ li {
+ a(href: sourceUrl(grailsGradlePublishVersion, 'grails-publish', '', 'grails-publish'), "Grails Publish Gradle Plugin $grailsGradlePublishVersion Source")
+ a(href: sourceVerificationUrl(grailsGradlePublishVersion, 'grails-publish', '.sha512', 'grails-publish'), 'SHA512')
+ a(href: sourceVerificationUrl(grailsGradlePublishVersion, 'grails-publish', '.asc', 'grails-publish'), 'ASC')
+ }
+ } else {
+ li {
+ a(
+ href: "https://github.com/apache/grails-forge/releases/download/v$version/grails-cli-${version}.zip",
+ 'Binary'
+ )
+ }
+ }
+ li {
+ a(
+ href: "https://github.com/apache/grails-core/releases/tag/v$version",
+ 'Grails Release Notes')
+ }
+ if (version.startsWith('7')) {
+ li {
+ a(
+ href: "https://github.com/apache/grails-spring-security/releases/tag/v$springSecurityVersion",
+ "Grails Spring Security Plugin $springSecurityVersion Release Notes")
+ }
+ li {
+ a(
+ href: "https://github.com/apache/grails-redis/releases/tag/v$redisVersion",
+ "Grails Redis $redisVersion Plugin Release Notes")
+ }
+ li {
+ a(
+ href: "https://github.com/apache/grails-quartz/releases/tag/v$quartzVersion",
+ "Grails Quartz $quartzVersion Plugin Release Notes"
+ )
+ }
+ li {
+ a(
+ href: "https://github.com/apache/grails-github-actions/releases/tag/v$grailsGithubActionsVersion",
+ "Grails GitHub Actions $grailsGithubActionsVersion Release Notes")
+ }
+ li {
+ a(
+ href: "https://github.com/apache/grails-gradle-publish/releases/tag/v$grailsGradlePublishVersion",
+ "Grails Publish Gradle Plugin $grailsGradlePublishVersion Release Notes"
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ @CompileDynamic
+ static String mainContent(File releases) {
+ def preRelease = SiteMap.latestPreReleaseVersion(releases)
+ def latest = SiteMap.latestVersion(releases)
+ renderHtml {
+ div(class: 'header-bar chalices-bg') {
+ div(class: 'content') {
+ h1('Downloads')
+ }
+ }
+ div(class: 'content') {
+ div(class: 'two-columns') {
+ div(class: 'odd column') {
+ h3(
+ class: 'column-header', style: 'margin-bottom: 10px',
+ 'Source and Binary Releases'
+ )
+ p {
+ mkp.yieldUnescaped(
+ 'We provide OpenPGP signatures (\'.asc\') files and checksums (\'.sha512\') for ' +
+ 'every release artifact. We recommend that you '
+ )
+ a(href: 'https://www.apache.org/info/verification.html', 'verify')
+ mkp.yieldUnescaped(
+ ' the integrity of downloaded files by generating your own checksums and match ' +
+ 'them against ours, and checking signatures using the '
+ )
+ a(href: 'https://www.apache.org/dyn/closer.lua/grails/KEYS?action=download', 'KEYS')
+ mkp.yieldUnescaped(' file which contains the Grails OpenPGP release keys.')
+ }
+ if (preRelease > latest) {
+ mkp.yieldUnescaped(DownloadPage.renderDownload(preRelease.versionText))
+ }
+
+ mkp.yieldUnescaped(DownloadPage.renderDownload('snapshot'))
+ mkp.yieldUnescaped(DownloadPage.renderDownload(latest.versionText))
+
+ h3(
+ class: 'column-header',
+ 'Older Versions'
+ )
+ p('You can download previous versions as far back as Grails 0.1.')
+ p(
+ 'NOTE: Versions prior to 7.0.0-M4 are not ASF releases. Links to those releases are ' +
+ 'provided here as a convenience.'
+ )
+ div(class: 'version-selector') {
+ select(class: 'form-control', onchange: 'window.location.href = this.value') {
+ option(label: 'Select a version', disabled: 'disabled', selected: 'selected')
+ SiteMap.stableVersions(releases)*.versionText.each {
+ option(value: DownloadPage.resolveOldDownloadUrl(it), it)
+ }
+ }
+ }
+ }
+ div(class: 'column') {
+
+ h3(
+ class: 'column-header',
+ style: 'margin-bottom: 10px',
+ 'Grails Application Forge'
+ )
+ p('The quickest way to get started with our application generator:')
+ p {
+ a(href: 'https://start.grails.org', 'Grails Application Forge')
+ }
+
+ h3(
+ class: 'column-header',
+ style: 'margin-bottom: 10px',
+ 'Installing with SDKMAN!'
+ )
+ p {
+ a(
+ href: 'https://sdkman.io/',
+ 'SDKMAN! (The Software Development Kit Manager)'
+ )
+ }
+
+ p(
+ 'This tool makes installing the Grails framework on any Unix based platform ' +
+ '(Mac OSX, Linux, Cygwin, Solaris, or FreeBSD) easy.'
+ )
+ p('Simply open a new terminal and enter:')
+ div(class: 'code', '$ curl -s https://get.sdkman.io | bash')
+ p('Follow the on-screen instructions to complete installation.')
+ p('Open a new terminal or type the command:')
+ div(class: 'code', '$ source "$HOME/.sdkman/bin/sdkman-init.sh"')
+ p('Then install the latest stable Grails version:')
+ div(class: 'code', '$ sdk install grails')
+ p(
+ 'If prompted, make this your default version. After installation is complete ' +
+ 'it can be tested with:'
+ )
+ div(class: 'code', '$ grails --version')
+ p('That\'s all there is to it!')
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy b/buildSrc/src/main/groovy/website/model/documentation/GuideGroup.groovy
similarity index 77%
copy from buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy
copy to buildSrc/src/main/groovy/website/model/documentation/GuideGroup.groovy
index 455b609980e..0d49bfe9196 100644
--- a/buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy
+++ b/buildSrc/src/main/groovy/website/model/documentation/GuideGroup.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,15 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails
+package website.model.documentation
import groovy.transform.CompileStatic
@CompileStatic
-class MarkdownPost extends Page {
+class GuideGroup implements GuideGroupHtml {
- @Override
- String getPath() {
- return filename.replace('.md', '.html').replace('.markdown', '.html')
- }
+ String description
+ String image
+ String title
+ String ulId
+
+ List<GuideGroupItem> items = []
}
diff --git a/buildSrc/src/main/groovy/website/model/documentation/GuideGroupHtml.groovy b/buildSrc/src/main/groovy/website/model/documentation/GuideGroupHtml.groovy
new file mode 100644
index 00000000000..c6e1fd06933
--- /dev/null
+++ b/buildSrc/src/main/groovy/website/model/documentation/GuideGroupHtml.groovy
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package website.model.documentation
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+
+import website.model.PageElement
+
+import static website.utils.RenderUtils.renderHtml
+
+@CompileStatic
+trait GuideGroupHtml implements PageElement {
+
+ abstract String getDescription()
+ abstract String getImage()
+ abstract String getUlId()
+ abstract String getTitle()
+
+ abstract List<GuideGroupItem> getItems()
+
+ @Override
+ String renderAsHtml() {
+ renderAsHtmlWithStyleAttr(null)
+ }
+
+ @CompileDynamic
+ String renderAsHtmlWithStyleAttr(String styleAttr) {
+ renderHtml {
+ omitNullAttributes = true
+ div(class: 'guide-group', style: styleAttr) {
+ div(class: 'guide-group-header') {
+ img(src: image, alt: title)
+ h2 {
+ mkp.yieldUnescaped(title)
+ }
+ }
+ ul(id: ulId) {
+ if (description) {
+ li(class: 'legend', description)
+ }
+ if (items) {
+ for (def item : items) {
+ if (item.href) {
+ li {
+ if (item.image) {
+ img(
+ class: 'align-left',
+ style: 'with: 70px; margin-right: 10px',
+ src: item.image,
+ alt: item.title
+ )
+ }
+ a(href: item.href, item.title)
+ if (item.legend) {
+ p(item.legend)
+ }
+ }
+ } else {
+ li(item.title)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy b/buildSrc/src/main/groovy/website/model/documentation/GuideGroupItem.groovy
similarity index 78%
copy from buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy
copy to buildSrc/src/main/groovy/website/model/documentation/GuideGroupItem.groovy
index 455b609980e..cb7c693a1ae 100644
--- a/buildSrc/src/main/groovy/org/grails/MarkdownPost.groovy
+++ b/buildSrc/src/main/groovy/website/model/documentation/GuideGroupItem.groovy
@@ -7,7 +7,7 @@
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
@@ -16,15 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.grails
+package website.model.documentation
import groovy.transform.CompileStatic
@CompileStatic
-class MarkdownPost extends Page {
+class GuideGroupItem {
- @Override
- String getPath() {
- return filename.replace('.md', '.html').replace('.markdown', '.html')
- }
+ String category
+ String href
+ String image
+ String legend
+ String title
}
diff --git a/buildSrc/src/main/groovy/website/model/documentation/ProfilesPage.groovy b/buildSrc/src/main/groovy/website/model/documentation/ProfilesPage.groovy
new file mode 100644
index 00000000000..f013ebf4f0f
--- /dev/null
+++ b/buildSrc/src/main/groovy/website/model/documentation/ProfilesPage.groovy
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package website.model.documentation
+
+import groovy.transform.CompileDynamic
+import groovy.transform.CompileStatic
+
+import org.yaml.snakeyaml.Yaml
+import website.utils.MarkdownUtils
+
+import static website.utils.RenderUtils.renderHtml
+
+@CompileStatic
+class ProfilesPage {
+
+ @CompileDynamic
+ static String mainContent(File profiles) {
+ renderHtml {
+ div(class: 'header-bar chalices-bg') {
+ div(class: 'content') {
+ h1('Profiles')
+ }
+ }
+ def categories = ProfilesPage.categories(profiles)
+ div(class: 'content') {
+ div(class: 'two-columns') {
+ div(class: 'odd column') {
+ mkp.yieldUnescaped(ProfilesPage.renderCategory(categories.find {
+ it.title == 'Profiles'
+ }))
+ }
+ div(class: 'column') {
+ mkp.yieldUnescaped(ProfilesPage.renderCategory(categories.find {
+ it.title == 'Plugin Profiles'
+ }))
+ mkp.yieldUnescaped(ProfilesPage.renderCategory(categories.find {
+ it.title == 'Third-Party Profiles'
+ }))
+
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads a YAML "profiles" model file and groups the contained profiles into categories.
+ * <p>
+ * The YAML file is expected to contain a top-level {@code profiles} map. Each entry under
+ * {@code profiles} represents one profile definition with at least {@code url} and {@code title},
+ * and may also define category metadata such as {@code category}, {@code image}, and
+ * {@code description}.
+ * </p>
+ * <p>
+ * This method iterates over all profile definitions, creates (or reuses) a {@link ProfilesCategory}
+ * for each distinct {@code category} value, copies category metadata (title/image/description)
+ * from the YAML entry, and adds a {@link Profile} (url/title) to the category’s {@code profiles}
+ * collection.
+ * </p>
+ *
+ * @param modules the YAML file containing the profiles model (must be readable)
+ * @return a collection of {@link ProfilesCategory} instances, each containing the profiles that
+ * belong to that category
+ */
+ private static Collection<ProfilesCategory> categories(File modules) {
+ def model = modules.newInputStream().withCloseable { is ->
+ new Yaml().load(is) as Map
+ }
+ Map<String, ProfilesCategory> byCategory = [:].withDefault { key ->
+ new ProfilesCategory(title: key)
+ }
+ (model.profiles as Map).each { k, v ->
+ def key = (v['category'] ?: 'Uncategorized') as String
+ def cat = byCategory[key]
+ cat.with {
+ image = v['image'] ?: null
+ description = v['description'] ?: null
+ }
+ cat.profiles << new Profile(url: v['url'], title: v['title'])
+ }
+ byCategory.values()
+ }
+
+ @CompileDynamic
+ private static String renderCategory(ProfilesCategory cat) {
+ if (!cat) return ''
+ renderHtml {
+ div(class: 'guide-group') {
+ div(class: 'guide-group-header') {
+ img(src: cat.image, alt: cat.title)
+ h2(cat.title)
+ }
+ ul {
+ if (cat.description) {
+ def legend = MarkdownUtils.htmlFromMarkdown(cat.description)
+ .replaceAll('<p>', '')
+ .replaceAll('</p>', '')
+
+ li(class: 'legend') {
+ mkp.yieldUnescaped(legend)
+ }
+ }
+ cat.profiles.each { item ->
+ li { a(href: item.url, item.title) }
+ }
+ }
+ }
+ }
+ }
+
+ static class ProfilesCategory {
+ String title
+ String image
+ String description
+ List<Profile> profiles = []
+ }
+
+ static class Profile {
+ String title
+ String url
+ }
+}
diff --git a/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy b/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy
new file mode 100644
index 00000000000..f29727071ed
--- /dev/null
+++ b/buildSrc/src/main/groovy/website/model/documentation/SiteMap.groovy
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package website.model.documentation
+
+import groovy.transform.CompileStatic
+
+import org.yaml.snakeyaml.Yaml
+
+@CompileStatic
+class SiteMap {
+
+ static List<SoftwareVersion> versions(File releases) {
+ assert releases.exists()
+ def model = releases.newInputStream().withCloseable {
+ new Yaml().load(it) as Map
+ }
+ (model.releases as List<Map>)
+ .collect { SoftwareVersion.build(it.version as String) }
+ .toSorted()
+ }
+
+ static SoftwareVersion latestVersion(File releases) {
+ stableVersions(releases)?.get(0)
+ }
+
+ static List<String> olderVersions(File releases) {
+ stableVersions(releases).tail()*.versionText
+ }
+
+ static List<SoftwareVersion> stableVersions(File releases) {
+ versions(releases)
+ .findAll { !it.isSnapshot() }
+ .toSorted { a, b -> b <=> a }
+ }
+
+ static List<SoftwareVersion> preReleaseVersions(File releases) {
+ versions(releases)
+ .findAll {it.getSnapshot()?.isMilestone() || it.getSnapshot()?.isReleaseCandidate() }
+ .toSorted { a, b -> b <=> a }
+ }
+
+ static SoftwareVersion latestPreReleaseVersion(File releases) {
+ preReleaseVersions(releases)?.get(0)
+ }
+}
diff --git a/buildSrc/src/main/groovy/website/model/documentation/Snapshot.groovy b/buildSrc/src/main/groovy/website/model/documentation/Snapshot.groovy
new file mode 100644
index 00000000000..99d5914b8df
--- /dev/null
+++ b/buildSrc/src/main/groovy/website/model/documentation/Snapshot.groovy
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package website.model.documentation
+
+import groovy.transform.CompileStatic
+
+@CompileStatic
+class Snapshot implements Comparable<Snapshot> {
+
+ final String text
+
+ Snapshot(String text) {
+ this.text = text
+ }
... 3083 lines suppressed ...