Here the python naming convention ( pep 008)
Sadly one of many such conventions which are out there. Best to use it as a guideline when forming your own conventions, but every org I have worked with makes it’s owns light tweaks.
No need to “invent” or discuss the coding convention as there already are thousands of pyRevit scripts out there with millions of lines of code. So not using this style for this pyRevit script collection would be crazy, and inventing some other style for the dynamo code collection also. So I`ll just stick to that, as I already do.
And we are live
Search with real time filter works, it`s a start!
Code is collected from github and opens in a dropdown box.
http://www.revitpythondocs.com/ (might take a while until availabe international)
I´ll try to make multiple versions of a script available through buttons in the code box, so they will only be listed one time…
And there should also be a version that uses inputs of a python node.
Buttons to switch between code versions work well
Page should now be available international.
I´ll add another set of buttons to toggle between desginscript and revit geometry if the script contains that.
I’m still recommend choose some template for build document like readthedocs. How you think @gerhard.p ?
Hello @chuongmep
Can you please explain how we would benefit from ReadTheDocs? I don´t quite get what it all can do, I see there are some interactions possible with GitHub. Making somethinhg like a “quick contribution” function so users can send code directly to GitHub is something i plan for the future.
I know ReadTheDocs only from pyRevit where it is used as a “manual” for pyRevit.
@jacob.small
I´m thinking about toggling comments for imports globally. So collecting code on Github without comments on imports and create them on demand in the code box. So they are equal in every code and can be easily modified in the future. But it would mean that comments in imports and in the rest of the code would have to be treated seperately.
It allow you can control version better, and don’t need too much to care about create a template from scratch
So the file name on github determines the buttons created on the web page.
You can add tags and geometry tags to the script name.
Active tag button will default to the first tag available, order is basic, advanced, function, example.
If an optional geometry tag is found it defaults to first geometry tag button, order is designscript, revit.
Am I missing something? Checkout the page and see if you have more ideas.
Only thing I can think of now is a button for comments.
Thats really cool…Gerhard thanks to you
ReadTheDocs doe sthe job but kind of lacks some love.
For the pyRevit documentation, we recently switch to mkdir and I went through all the docstrings to homogenize the whole thing and make the example work and all.
The whole things is set up to publish with github actions whenever a PR is merged into the ‘docs’ branch.
Designwise it is much cleaner.
Good to know that I can use some prebuilt template if I´ll fail at building a webpage from scratch! It´s not that easy but really intreresting if you never done it before.
So far everything works just fine, I´m just going a little too crazy with the colors, it´s just a phase
Next steps:
- adding a toggle to the search bar to switch between script title and content search.
- adding copy to clipboard and max size with scrollbar to the code dropouts.
- adding an “other…” button to the code dropout. This will show/open all other versions of this code if there are any. For now every script has the 4 standard versions directly accessible to dont overload things, but users can share more versions under the “other…” category.
- make decisions regarding code color and code background color, maybe making a toggle to switch themes.
- adding and hiding comments
- Full implement data grabbing with github API. Currently done with a local python file.
I think that´s it, not many more functions needed.
Edit: Oh and I have to make sure the page also looks good on the phone.
Getting there, title and content search with highlighting works. Just struggling a little with changing tabs if the keyword is not on the default tab…
Some debugging to do though.
Really looking forward now to going back to python^^
Some java:
// Function to check if cached data is still valid
function isCacheValid(cacheTimestamp) {
const now = new Date().getTime();
const twoHours = 2 * 60 * 60 * 1000; // 2 hours in milliseconds
return (now - cacheTimestamp) < twoHours;
}
function wrapTextNodesWithHighlight(element, searchTerm) {
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
let node;
while (node = walker.nextNode()) {
const nodeValue = node.nodeValue;
const lowerCaseNodeValue = nodeValue.toLowerCase();
if (lowerCaseNodeValue.includes(searchTerm.toLowerCase())) {
const frag = document.createDocumentFragment();
let lastIndex = 0;
let match;
const regex = new RegExp(`(${searchTerm})`, 'gi');
while ((match = regex.exec(nodeValue)) !== null) {
const matchedText = match[0];
const matchedIndex = match.index;
// Add preceding text
frag.appendChild(document.createTextNode(nodeValue.substring(lastIndex, matchedIndex)));
// Add highlighted text
const highlightSpan = document.createElement('span');
highlightSpan.classList.add('highlighted-term');
highlightSpan.textContent = matchedText;
frag.appendChild(highlightSpan);
lastIndex = matchedIndex + matchedText.length;
}
// Add any remaining text
frag.appendChild(document.createTextNode(nodeValue.substring(lastIndex)));
// Replace the original text node with the fragment
const parent = node.parentNode;
parent.replaceChild(frag, node);
}
}
}
function setDefaultSearchTypeButton() {
// Set the title search button as active by default
const titleSearchButton = document.querySelector('.search-type-button[data-search-type="title"]');
if (titleSearchButton) {
titleSearchButton.classList.add('active');
}
}
function performSearch() {
const searchTerm = document.getElementById('search-bar').value.toLowerCase();
const listItems = document.querySelectorAll('#python-scripts-list li');
// Clear previous results and highlights
new Mark(document.querySelectorAll('#python-scripts-list .language-python')).unmark({
done: function () {
// If the search term is cleared, reset to default view
if (!searchTerm) {
listItems.forEach(item => {
item.style.display = ''; // Show all items
// Reset active state and dropdown display
const scriptButton = item.querySelector('.script-button');
const dropdownContent = item.querySelector('.dropdown-content');
scriptButton && scriptButton.classList.remove('active');
dropdownContent && (dropdownContent.style.display = 'none');
});
return; // Exit the function early
}
const searchType = document.querySelector('.search-type-button.active').getAttribute('data-search-type');
// Proceed with highlighting only for code search
if (searchType === "code") {
new Mark(document.querySelectorAll('#python-scripts-list .language-python')).mark(searchTerm, {
"element": "span",
"className": "highlighted-term",
done: function () {
filterItems(searchTerm, listItems, searchType);
}
});
} else {
// For title search, no marking is needed, just filter the items
filterItems(searchTerm, listItems, searchType);
}
}
});
}
function filterItems(searchTerm, listItems, searchType) {
listItems.forEach(item => {
let isMatch = false;
if (searchType === "code") {
if (item.code && item.code.toLowerCase().includes(searchTerm)) {
isMatch = true;
}
} else {
// Title search
const titleText = item.querySelector('.script-button').textContent.toLowerCase();
isMatch = titleText.includes(searchTerm);
}
// Set button to active and display dropdown for code matches
const scriptButton = item.querySelector('.script-button');
const dropdownContent = item.querySelector('.dropdown-content');
if (isMatch && searchType === "code") {
scriptButton && scriptButton.classList.add('active');
dropdownContent && (dropdownContent.style.display = 'block');
// Scroll to the first highlighted element within the dropdown's code block
const firstHighlighted = dropdownContent.querySelector('.highlighted-term');
if (firstHighlighted) {
const scrollableContainer = dropdownContent.querySelector('.language-python'); // Adjust this selector to target the correct scrollable container
if (scrollableContainer) {
// Calculate the position to scroll to, centering the highlighted text
const highlightedPosition = firstHighlighted.offsetTop;
const containerScrollTop = scrollableContainer.scrollTop;
const containerHeight = scrollableContainer.clientHeight;
const scrollTo = highlightedPosition + containerScrollTop - containerHeight / 2 + firstHighlighted.offsetHeight / 2;
scrollableContainer.scrollTop = scrollTo;
}
}
} else {
scriptButton && scriptButton.classList.remove('active');
dropdownContent && (dropdownContent.style.display = 'none');
}
item.style.display = isMatch ? '' : 'none';
});
}
// Function to load data either from cache or via fetch
async function loadData(jsonFile, cacheKey) {
const cachedData = localStorage.getItem(cacheKey);
const cacheTimestamp = localStorage.getItem(cacheKey + '_timestamp');
if (cachedData && cacheTimestamp && isCacheValid(parseInt(cacheTimestamp, 10))) {
return JSON.parse(cachedData);
} else {
try {
const response = await fetch(jsonFile);
const data = await response.json();
localStorage.setItem(cacheKey, JSON.stringify(data));
localStorage.setItem(cacheKey + '_timestamp', new Date().getTime().toString());
return data;
} catch (error) {
console.error('Error loading data:', error);
return null;
}
}
}
// Global variables to store JSON data
var dynamoData = null;
var pyRevitData = null;
document.addEventListener('DOMContentLoaded', async function () {
// Load data as before
dynamoData = await loadData('dynamo_scripts_data.json', 'dynamoData');
pyRevitData = await loadData('pyrevit_scripts_data.json', 'pyRevitData');
// Set default active tab to 'Dynamo' and load its content
var defaultTab = document.getElementsByClassName("tablinks")[0];
defaultTab.click();
processData(dynamoData); // Process Dynamo data as default
// Add event listeners after defining performSearch
addEventListenersToVersionButtons();
setDefaultActiveButton();
addSearchTypeEventListeners();
setDefaultSearchTypeButton();
// Set the title search button as active by default using its ID
const titleSearchButton = document.querySelector('.search-type-button[data-search-type="title"]')
if (titleSearchButton) {
titleSearchButton.classList.add('active');
} else {
console.error('Title search button not found');
}
});
function processData(data) {
const pythonScriptsList = document.getElementById('python-scripts-list');
pythonScriptsList.innerHTML = ''; // Clear existing content
const scriptsMap = new Map();
data.forEach(script => {
const baseNameMatch = script.name.match(/^(.+?)_/);
const baseName = baseNameMatch ? baseNameMatch[1] : script.name;
const fileNameParts = script.name.split('_');
const formattedScriptName = baseName.replace(/([a-z])([A-Z])/g, '$1 $2');
if (!scriptsMap.has(baseName)) {
scriptsMap.set(baseName, {
formattedName: formattedScriptName,
versions: [],
tags: new Set(),
geometryTags: new Set()
});
}
const scriptGroup = scriptsMap.get(baseName);
scriptGroup.versions.push(script);
fileNameParts.forEach(part => {
if (part.match(/(basic|advanced|function|example)/)) {
scriptGroup.tags.add(part);
} else if (part.includes('designscript')) {
scriptGroup.geometryTags.add('designscript');
} else if (part.includes('revit')) {
scriptGroup.geometryTags.add('revit');
}
});
});
scriptsMap.forEach((scriptGroup, baseName) => {
// Example of creating a list item with a script button and a code preview
const listItem = document.createElement('li');
const scriptButton = document.createElement('button');
scriptButton.textContent = scriptGroup.formattedName;
scriptButton.classList.add('script-button');
if (scriptGroup.versions[0] && scriptGroup.versions[0].content) {
listItem.code = scriptGroup.versions[0].content; // Assign the code
} else {
console.error('No content found for version:', scriptGroup.versions[0]);
}
const codePreview = document.createElement('div');
codePreview.classList.add('code-preview');
codePreview.textContent = scriptGroup.versions[0].content; // Example content
codePreview.style.display = 'none'; // Hide by default
listItem.appendChild(scriptButton);
listItem.appendChild(codePreview);
const dropdownWrapper = document.createElement('div');
const dropdownContent = document.createElement('div');
const scriptTextContainer = document.createElement('pre');
const codeElement = document.createElement('code');
codeElement.className = 'language-python';
scriptTextContainer.appendChild(codeElement);
const buttonContainer = document.createElement('div');
const tagButtonContainer = document.createElement('div'); // Container for tag buttons
const geometryTagButtonContainer = document.createElement('div'); // Container for geometry tag buttons
dropdownWrapper.classList.add('dropdown-wrapper');
dropdownContent.classList.add('dropdown-content');
buttonContainer.classList.add('button-container');
tagButtonContainer.classList.add('tag-button-container');
geometryTagButtonContainer.classList.add('geometry-tag-button-container');
dropdownContent.style.display = 'none';
// Script text container style
scriptTextContainer.style.position = 'relative'; // Make sure the container has relative positioning
scriptTextContainer.style.paddingTop = '30px'; // Add padding to the top to avoid overlapping with the button
// Create the copy button
const copyButton = document.createElement('button');
copyButton.textContent = 'Copy';
copyButton.classList.add('copy-button');
copyButton.style.position = 'absolute';
copyButton.style.top = '10px'; // Adjust the position as needed
copyButton.style.right = '10px'; // Adjust the position as needed
copyButton.style.zIndex = '10'; // Ensure the button is above other elements
// Add event listener to the copy button
copyButton.addEventListener('click', function () {
copyToClipboard(codeElement.textContent);
});
// Append the copy button to the scriptTextContainer
scriptTextContainer.appendChild(copyButton);
scriptTextContainer.style.position = 'relative'; // To position the copy button correctly
// Create tag buttons in the specified order and add to tagButtonContainer
const orderedTags = ['basic', 'advanced', 'function', 'example'];
let firstTagActiveSet = false;
orderedTags.forEach(tag => {
if (scriptGroup.tags.has(tag)) {
const tagButton = createTagButton(tag, scriptGroup, scriptTextContainer);
tagButtonContainer.appendChild(tagButton); // Append to tagButtonContainer
if (!firstTagActiveSet) {
tagButton.classList.add('active');
const matchingScript = scriptGroup.versions.find(v => v.name.includes(tag));
if (matchingScript) {
const codeElement = scriptTextContainer.querySelector('code');
if (codeElement) {
codeElement.textContent = matchingScript.content; // Correctly updating the code element
Prism.highlightElement(codeElement);
} else {
console.error('Code element not found');
}
}
firstTagActiveSet = true;
}
}
});
// Create geometry tag buttons in the specified order and add to tagButtonContainer
// Initialize a counter before the loop
let geometryTagIndex = 0;
scriptGroup.geometryTags.forEach((tag, index) => {
const isActive = (geometryTagIndex === 0);
const geometryTagButton = createGeometryTagButton(tag, scriptGroup, scriptTextContainer, isActive);
geometryTagButtonContainer.appendChild(geometryTagButton); // Append to geometryTagButtonContainer
if (isActive) {
geometryTagButton.classList.add('active');
const matchingScript = scriptGroup.versions.find(v => v.name.includes(tag));
if (matchingScript) {
const codeElement = scriptTextContainer.querySelector('code');
if (codeElement) {
codeElement.textContent = matchingScript.content; // Correctly updating the code element
Prism.highlightElement(codeElement);
} else {
console.error('Code element not found');
}
}
}
// Increment the counter
geometryTagIndex++;
});
// Append tagButtonContainer and geometryTagButtonContainer to buttonContainer
buttonContainer.appendChild(tagButtonContainer);
if (scriptGroup.geometryTags.size > 0) {
// Append the geo tags to buttonContainer
buttonContainer.appendChild(geometryTagButtonContainer);
}
// Append elements to the dropdown
dropdownWrapper.appendChild(buttonContainer);
dropdownWrapper.appendChild(scriptTextContainer);
dropdownContent.appendChild(dropdownWrapper);
listItem.appendChild(dropdownContent);
pythonScriptsList.appendChild(listItem);
scriptButton.addEventListener('click', function () {
dropdownContent.style.display = dropdownContent.style.display === 'none' ? 'block' : 'none';
this.classList.toggle('active');
});
});
// Existing searchBar keyup and radio buttons change event listeners
const searchBar = document.getElementById('search-bar');
searchBar.addEventListener('keyup', performSearch);
const searchTypeRadios = document.querySelectorAll('input[name="searchType"]');
searchTypeRadios.forEach(radio => {
radio.addEventListener('change', performSearch);
});
}
function copyToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
} catch (err) {
console.error('Unable to copy', err);
}
document.body.removeChild(textArea);
}
function createTagButton(tagName, scriptGroup, scriptTextContainer) {
const button = document.createElement('button');
button.textContent = tagName;
button.classList.add('tag-button');
button.addEventListener('click', function () {
const buttonContainer = this.closest('.button-container');
const activeGeometryTagButton = buttonContainer.querySelector('.geometry-tag-button.active');
const activeGeometryTag = activeGeometryTagButton ? activeGeometryTagButton.textContent : null;
const matchingScript = scriptGroup.versions.find(v => v.name.includes(tagName) && (!activeGeometryTag || v.name.includes(activeGeometryTag)));
if (matchingScript) {
// Retrieve the <code> element
const codeElement = scriptTextContainer.querySelector('code');
if (codeElement) {
// Update the content of the <code> element, not the entire scriptTextContainer
codeElement.textContent = matchingScript.content;
Prism.highlightElement(codeElement);
} else {
console.error('Code element not found');
}
}
buttonContainer.querySelectorAll('.tag-button').forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
});
return button;
}
function createGeometryTagButton(tagName, scriptGroup, scriptTextContainer, isActive) {
const button = document.createElement('button');
button.textContent = tagName;
button.classList.add('geometry-tag-button');
if (isActive) {
button.classList.add('active');
}
button.addEventListener('click', function () {
const buttonContainer = this.closest('.button-container');
const activeTagButton = buttonContainer.querySelector('.tag-button.active');
const activeTag = activeTagButton ? activeTagButton.textContent : null;
const matchingScript = scriptGroup.versions.find(v => v.name.includes(activeTag) && v.name.includes(tagName));
if (matchingScript) {
// Retrieve the <code> element
const codeElement = scriptTextContainer.querySelector('code');
if (codeElement) {
// Update the content of the <code> element, not the entire scriptTextContainer
codeElement.textContent = matchingScript.content;
Prism.highlightElement(codeElement);
} else {
console.error('Code element not found');
}
}
buttonContainer.querySelectorAll('.geometry-tag-button').forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
});
return button;
}
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
// Get all elements with class="tabcontent" and hide them
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
// Get all elements with class="tablinks" and remove the class "active"
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
// Show the current tab, and add an "active" class to the button that opened the tab
document.getElementById(tabName).style.display = "block";
evt.currentTarget.className += " active";
// Process data based on the active tab
if (tabName === 'Dynamo' && dynamoData) {
processData(dynamoData);
} else if (tabName === 'pyRevit' && pyRevitData) {
processData(pyRevitData);
}
}
function setActiveButton(evt) {
var versionButtons = document.getElementsByClassName("version-button");
// Remove "active" class from all buttons
for (var i = 0; i < versionButtons.length; i++) {
versionButtons[i].classList.remove("active");
}
// Add "active" class to the clicked button
evt.currentTarget.classList.add("active");
}
// Add event listeners to each version-button
function addEventListenersToVersionButtons() {
var versionButtons = document.getElementsByClassName("version-button");
for (var i = 0; i < versionButtons.length; i++) {
versionButtons[i].addEventListener('click', setActiveButton);
}
}
// Set the first version-button as active by default
function setDefaultActiveButton() {
var versionButtons = document.getElementsByClassName("version-button");
if (versionButtons.length > 0) {
versionButtons[0].classList.add("active");
}
}
document.addEventListener('DOMContentLoaded', (event) => {
addEventListenersToVersionButtons();
setDefaultActiveButton();
addSearchTypeEventListeners();
setDefaultSearchTypeButton();
});
function addSearchTypeEventListeners() {
const searchTypeButtons = document.querySelectorAll('.search-type-button');
searchTypeButtons.forEach(button => {
button.addEventListener('click', function () {
// Remove active class from all buttons
searchTypeButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
// Perform search immediately after setting the button active
performSearch();
});
});
}
Current progress only in tiny steps, would need a few days off to boost this project. But christmas holidays are ahead!
Spent most of the time to work out the “coding convention”, error handling, etc…but I´m on the right track now!
Currently setting up the scripts…
File names are getting longer:
GetViewTemplates_Basic_ironpython_cpython_2022_2023_2024
Changes to the page:
I´ll rename “advanced” code version to “Exception” because this is just about raising errors.
At creating more codes I now see the need for comments and docstrings. The script title is not enough to explain what the purpose is exactly.
I will kill the sidebar buttons for revit and python versions.
I´ll better integrate that directly to every script, this will also make it possible to see if a code has changed from one revit version to the next, or if ironpython/Cpython are equal or different (This will all be visible by the button colors.)
Maybe something like that:
One thing i don´t really know is how i should make it possible to link to a specific script…
And I´ll open up another thread for the challenge of testing thousands of codes for a new revit version.
My goal is to finish this in december and we can start wirth contributions with the new year. I`m already interrested how fast this will grow.
After a “little” break I´m looking forward to finish this project this week.
It was a pain to get the filtering for year/python/version/geometry working but I did it finally. For sure I had to start the code new from beginning.
Will reorder the buttons as they need too much space now.
It was a hard timeline I set myself but WE ARE LIVE
https://www.revitpythondocs.com/
- Full automatic creating content from github files.
- Currently updated every 60 minutes.
- Share your files on Github, pull requests for adding files will be pushed to the webpage automatic.
- If you want to contribute without github just send me the files here on the forum.
Please follow the readme (rough draft) on github, contribute a script/file and give some feedback. Thanks!
https://github.com/GerhardPaw/RevitPythonDocs
(Automatic merging of pull requests is not tested by now, will do this tomorrow.)
(Some minor issues regarding activating/deactivating of buttons when switching between search options or dynamo/pyrevit content are still present but will be debugged soon.)