RevitPythonDocs for Dynamo and pyRevit - Request for Feedback

Here the python naming convention ( pep 008)

1 Like

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 :slight_smile:
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.

4 Likes

Buttons to switch between code versions work well :slight_smile:
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.

1 Like

I’m still recommend choose some template for build document like readthedocs. How you think @gerhard.p ?

1 Like

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.

https://pyrevit.readthedocs.io/

@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.

1 Like

It allow you can control version better, and don’t need too much to care about create a template from scratch

2 Likes

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.

1 Like

Thats really cool…Gerhard thanks to you

2 Likes

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.

https://ein.sh/pyRevit/reference/pyrevit/

1 Like

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 :smiley:

image

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.

7 Likes

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. :slight_smile:

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();
    });
  });
}
2 Likes

Creating a logo with chat gpt :sweat_smile:

image

image

image

image

2 Likes

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.

2 Likes

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.

4 Likes

It was a hard timeline I set myself but WE ARE LIVE :partying_face:

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.)

7 Likes