Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs: Add client-side redirect to new docs. #30774

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from

Conversation

Mugen87
Copy link
Collaborator

@Mugen87 Mugen87 commented Mar 21, 2025

Related issue: #30748

Description

The PR is a first attempt to implement a client-side redirect from the old to the new documentation.

Since the old documentation is iFrame based, I've realized there is a single spot for implementing a redirect in index.html.

The build path of the new docs has been updated. The docs are not built into /docs_new/<package>/<version> anymore but just /docs_new. That makes the setup a bit simpler.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 24, 2025

@mrdoob For the next release r175, I recommend the following steps:

  • Publish the new docs via GitHub Pages so they are accessible via https://threejs.org/docs_new.
  • Update the links to the documentation at the homepage and GitHub to https://threejs.org/docs_new.
  • As discussed in Remove old docs. #30748, keep http://threejs.org/docs hosted and use this PR to redirect to the new doc pages.

@mrdoob
Copy link
Owner

mrdoob commented Mar 25, 2025

The build path of the new docs has been updated. The docs are not built into /docs_new/<package>/<version> anymore but just /docs_new. That makes the setup a bit simpler.

Great! Yeah, I was going to ask why did we need <package>/<version> in the url.

Just had a look a the new docs (finally)... They're looking pretty good!

However, I think they still need a few more tweaks (which I will do for r176).
Lets wait until r176 for the redirection code.

In the meantime...

Now that we have a jsdocs, can you investigate if we're able to generate a llms.txt too?

@Mugen87 Mugen87 added this to the r176 milestone Mar 25, 2025
@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 26, 2025

Now that we have a jsdocs, can you investigate if we're able to generate a llms.txt too?

When I understand the description of https://llmstxt.org/ correctly, the idea is to generate for each HTML documentation page a markdown version. So for AmbientLight.html there would be additionally AmbientLight.html.md and so on. The contents would be the same just formatted differently.

Yes, we can use JSDoc to generate such files but that requires a complete separate JSDoc template. The predefined tmpl template files of the default JSDoc theme can't be used since they output HTML. So all tmpl files must be migrated to Markdown. The default template also uses some JS helper functions (e.g. for link generation) that assume HTML output. These must be updated as well.

When thinking about this: If such a LLM JSDoc template is developed, it should be a separate project since any JSDoc documentation could use it to generate LLM friendly markdown files.

However, developing a new template is maybe not necessary since there is an existing project for generating markdown from JSDoc: https://github.com/jsdoc2md/jsdoc-to-markdown

I've tried to convert AudioListener to markdown with that tool (jsdoc2md src/audio/AudioListener.js) and below is the outcome. Unfortunately, it is not fully spec conform (at least the title must be h1). But this can be solved with a post-processing step or by configuring jsdoc-to-markdown (it seems to have some customization options). I wonder if someone from the llmstxt team could have a look at the below Markdown and recommend what to change.

<a name="AudioListener"></a>

## AudioListener ⇐ <code>Object3D</code>
The class represents a virtual listener of the all positional and non-positional audio effects
in the scene. A three.js application usually creates a single listener. It is a mandatory
constructor parameter for audios entities like [Audio](Audio) and [PositionalAudio](PositionalAudio).

In most cases, the listener object is a child of the camera. So the 3D transformation of the
camera represents the 3D transformation of the listener.

**Kind**: global class  
**Extends**: <code>Object3D</code>  

* [AudioListener](#AudioListener) ⇐ <code>Object3D</code>
    * [new AudioListener()](#new_AudioListener_new)
    * [.context](#AudioListener+context) : <code>AudioContext</code>
    * [.gain](#AudioListener+gain) : <code>GainNode</code>
    * [.filter](#AudioListener+filter) : <code>AudioNode</code>
    * [.timeDelta](#AudioListener+timeDelta) : <code>number</code>
    * [.getInput()](#AudioListener+getInput) ⇒ <code>GainNode</code>
    * [.removeFilter()](#AudioListener+removeFilter) ⇒ [<code>AudioListener</code>](#AudioListener)
    * [.getFilter()](#AudioListener+getFilter) ⇒ <code>AudioNode</code>
    * [.setFilter(value)](#AudioListener+setFilter) ⇒ [<code>AudioListener</code>](#AudioListener)
    * [.getMasterVolume()](#AudioListener+getMasterVolume) ⇒ <code>number</code>
    * [.setMasterVolume(value)](#AudioListener+setMasterVolume) ⇒ [<code>AudioListener</code>](#AudioListener)

<a name="new_AudioListener_new"></a>

### new AudioListener()
Constructs a new audio listener.

<a name="AudioListener+context"></a>

### audioListener.context : <code>AudioContext</code>
The native audio context.

**Kind**: instance property of [<code>AudioListener</code>](#AudioListener)  
**Read only**: true  
<a name="AudioListener+gain"></a>

### audioListener.gain : <code>GainNode</code>
The gain node used for volume control.

**Kind**: instance property of [<code>AudioListener</code>](#AudioListener)  
**Read only**: true  
<a name="AudioListener+filter"></a>

### audioListener.filter : <code>AudioNode</code>
An optional filter.

Defined via [setFilter](#AudioListener+setFilter).

**Kind**: instance property of [<code>AudioListener</code>](#AudioListener)  
**Default**: <code>null</code>  
**Read only**: true  
<a name="AudioListener+timeDelta"></a>

### audioListener.timeDelta : <code>number</code>
Time delta values required for `linearRampToValueAtTime()` usage.

**Kind**: instance property of [<code>AudioListener</code>](#AudioListener)  
**Default**: <code>0</code>  
**Read only**: true  
<a name="AudioListener+getInput"></a>

### audioListener.getInput() ⇒ <code>GainNode</code>
Returns the listener's input node.

This method is used by other audio nodes to connect to this listener.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: <code>GainNode</code> - The input node.  
<a name="AudioListener+removeFilter"></a>

### audioListener.removeFilter() ⇒ [<code>AudioListener</code>](#AudioListener)
Removes the current filter from this listener.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: [<code>AudioListener</code>](#AudioListener) - A reference to this listener.  
<a name="AudioListener+getFilter"></a>

### audioListener.getFilter() ⇒ <code>AudioNode</code>
Returns the current set filter.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: <code>AudioNode</code> - The filter.  
<a name="AudioListener+setFilter"></a>

### audioListener.setFilter(value) ⇒ [<code>AudioListener</code>](#AudioListener)
Sets the given filter to this listener.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: [<code>AudioListener</code>](#AudioListener) - A reference to this listener.  

| Param | Type | Description |
| --- | --- | --- |
| value | <code>AudioNode</code> | The filter to set. |

<a name="AudioListener+getMasterVolume"></a>

### audioListener.getMasterVolume() ⇒ <code>number</code>
Returns the applications master volume.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: <code>number</code> - The master volume.  
<a name="AudioListener+setMasterVolume"></a>

### audioListener.setMasterVolume(value) ⇒ [<code>AudioListener</code>](#AudioListener)
Sets the applications master volume. This volume setting affects
all audio nodes in the scene.

**Kind**: instance method of [<code>AudioListener</code>](#AudioListener)  
**Returns**: [<code>AudioListener</code>](#AudioListener) - A reference to this listener.  

| Param | Type | Description |
| --- | --- | --- |
| value | <code>number</code> | The master volume to set. |

@mrdoob
Copy link
Owner

mrdoob commented Mar 26, 2025

Ah, I was searching for "jsdoc to llms.txt" but "jsdoc to markdown" makes more sense.

It actually needs to be in a single file. The whole documentation with all the classes in a single llms.txt file.

People developing using cursor and windsurf can then add the file for the llm/agent:

Screenshot 2025-03-26 at 19 54 01 Screenshot 2025-03-26 at 19 54 22

And yes, this would definitely be a different thing from npm run build-docs.

@mrdoob
Copy link
Owner

mrdoob commented Mar 26, 2025

Thinking about the jsdoc template system...

Could we make it so jsdocs generate files like these?
https://github.com/mrdoob/three.js/blob/dev/docs/api/en/audio/AudioListener.html

That way we could just transparently replace the current documentation without having to do any redirects... 🤔

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 26, 2025

I've never seen a JSDoc-based documentation like that since the entire linking logic of JSDoc assumes all doc pages are located in the same directory. I recommend to use the JSDoc standard instead of building a custom solution.

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 26, 2025

It actually needs to be in a single file. The whole documentation with all the classes in a single llms.txt file.

I'll try to figure out a script utilizing jsdoc2md that does that for us. 🙌

@Mugen87
Copy link
Collaborator Author

Mugen87 commented Mar 27, 2025

Below is the mentioned script and also the generated file llms.txt as a zip. Can you check if it is compatible with the mentioned tools?

llms.txt.zip

import fs from 'fs/promises';
import path from 'path';

import jsdoc2md from 'jsdoc-to-markdown';

// This script converts the API documentation from JSDoc to Markdown and writes everything
// into a single build/llms.txt output file.

build();

async function build() {

	// gather all files potentially holding JS Doc

	const files = [];

	const config = JSON.parse( await fs.readFile( 'utils/docs/jsdoc.config.json', 'utf8' ) );

	const includes = config.source.include;
	const excludes = config.source.exclude;

	for ( const include of includes ) {

		const includeFiles = await toArray( readAllFiles( include ) ); // Replace toArray() with Array.fromAsync() when supported

		// filter out excluded and non-js files

		files.push( ... includeFiles.filter( function ( file ) {

			for ( const exclude of excludes ) {

				if ( file.startsWith( exclude ) === true || file.endsWith( '.js' ) === false ) {

					return false;

				}

			}

			return true;

		} ) );

	}

	// create output directory if not existing

	const outputDir = 'utils/llm/build';
	if ( await fs.stat( outputDir ) === false ) await fs.mkdir( outputDir );

	// create output path and rested existing file

	const outputPath = path.join( outputDir, 'llms.txt' );
	await fs.writeFile( outputPath, '', { encoding: 'utf8', flag: 'w' } );

	// convert JSDoc to Markdown and save in output file

	await generateTitle( outputPath );
	await generateAPI( outputPath, files );

}

// helpers

async function generateTitle( outputPath ) {

	await fs.appendFile( outputPath, '# three.js \n\n' );
	await fs.appendFile( outputPath, '> JavaScript 3D Library. \n\n' );

}

async function generateAPI( outputPath, files ) {

	for ( const file of files ) {

		await fs.appendFile( outputPath, await jsdoc2md.render( { files: file } ) );

	}

}

async function* readAllFiles( dir ) {

	const files = await fs.readdir( dir, { withFileTypes: true } );

	for ( const file of files ) {

		if ( file.isDirectory() ) {

			yield* readAllFiles( path.join( dir, file.name ) );

		} else {

			yield path.join( dir, file.name );

		}

	}


}

async function toArray( asyncIterator ) {

	const arr = [];
	for await ( const i of asyncIterator ) arr.push( i );
	return arr;

}

If the script works out, I'll file a PR. It is supposed to be located in utils/llm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants