4umi.com/web/javascript/filewrite

Write-a-file

Useful Javascript

Internet Explorer lets you write to the Windows file system by means of the harmless execCommand() function. It does not rely on potentially unsafe and often crippled ActiveX technology which is needed for reading files or anything locally. Nothing will be saved without express prior consent, a dialog allowing the selection of a folder requires the user to click or type ‘OK’ each time, and there is even an extra request for confirmation should the filename already exist. Because, if that is confirmed, the file will be overwritten.

A live example form

Enter the text to be saved, and optionally a name. Then click the ‘Save’ button to save the text to a local file on your system. Use the ‘Unnull’ function if you have a utf-16 encoded text with null-bytes that needs cleaning up.
Your text
 

File types

Two types of text files are available for saving to: .txt for plain text and .html for rich text documents, ie. containing some markup. The "SaveAs" dialog window accessible with the execCommand() function comes from a standard system dll and is not the same dialog that the browser uses when Save As... is chosen from the File menu. Only the latter includes the famous .mht single-file option.

Files saved as .html will be wrapped in <body> tags. The head will look like this:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML><HEAD>
<META http-equiv=Content-Type content="text/html;charset=iso-8859-1"></HEAD>
<BODY>

The footer like this:

</BODY></HTML>

It is not possible to change the doctype or to save anything else in the document head section. Tags will be made uppercase and whitespace normalized. The filename may end in .htm depending on the preference set in your Folder Options....

It will be clear that control is very limited and more advanced uses, with filenames ending in .css for instance, are ruled out. Writing to the local filesystem with absolute power requires an ActiveXObject and the 'Scripting.FileSystemObject'.

The script

The script is called upon submission of the form:

<form action="#" onsubmit="return savefile(this);">

Only whole documents can be saved with this method, so the text must be copied to a separate window or frame or some such object; this script uses an hidden <iframe> element. The script creates the iframe if it doesn't already exist or opens a small popup window if it cannot be created, writes the text to it, and then calls the execCommand() function as a method of that newly created document.

The execCommand() method takes three arguments. The first is the command identifier, a string specifying the command to execute. The second parameter is a boolean indicating whether a dialog should be displayed. With SaveAs as the comand, which always displays a dialog, it is optional contrary to the documentation on the manufacturer's website. The required path and name of the file to create are passed in the third parameter, with the backslash character \ as separator.

If the user at some point canceled the Save dialog, the method returns false. If the specified location is marked as read-only or for some technical reason the command did not execute, nothing is saved and the file has not been written, the same false is returned. It is not possible to determine in the script which of the two is returned, except by asking. If, by the grace of God, the file is saved, the method returns a sure true.

function savefile( f ) {
 f = f.elements;  //  reduce overhead

 var w = window.frames.w;
 if( !w ) {
  w = document.createElement( 'iframe' );
  w.id = 'w';
  w.style.display = 'none';
  document.body.insertBefore( w, null );
  w = window.frames.w;
  if( !w ) {
   w = window.open( '', '_temp', 'width=100,height=100' );
   if( !w ) {
    window.alert( 'Sorry, the file could not be created.' ); return false;
   }
  }
 }

 var d = w.document,
  ext = f.ext.options[f.ext.selectedIndex],
  name = f.filename.value.replace( /\//g, '\\' ) + ext.text;

 d.open( 'text/plain', 'replace' );
 d.charset = ext.value;
 if( ext.text==='.txt' ) {
  d.write( f.txt.value );
  d.close();
 } else {  //  '.html'
  d.close();
  d.body.innerHTML = '\r\n' + f.txt.value + '\r\n';
 }

 if( d.execCommand( 'SaveAs', null, name ) ){
  window.alert( name + ' has been saved.' );
 } else {
  window.alert( 'The file has not been saved.\nIs there a problem?' );
 }
 w.close();
 return false;  //  don't submit the form
}

The last call to close() affects a popup, but not an iframe.

Null bytes

 Sample code with null-bytes.
Sample code with null-bytes.

The ‘zero’ bytes that appear like extra spaces between the characters using the default UTF-16 Unicode encoding, may be surpressed by opening the file as text/plain rather than text/html, regardless of extension, and forcing the character set to UTF-8 as shown in the Javascript function above.

Should you happen to have some on your clipboard, a quick regular expression to remove this excess whitespace follows:

  1. newstring = oldstring.replace( /(\s(.))/g, '$2' );

The reverse, adding a space before every character, is achieved with this regex:

  1. newstring = oldstring.replace( /(.)/g, ' $1' );

The function behind the ‘Unnull’ button in the form on this page is built on this theme:

function unnull(s) {
 var r = new RegExp( '^\s*' + String.fromCharCode( 255, 254 ) );
 return r.test( s ) || window.confirm( 'This does not appear to be a UTF-16 encoded text.\nProceed anyway?' ) ?
  s.replace( r, '' ).replace( /((\r?)\n )\1/g, '$2\n' ).replace( /(\s(.))/g, '$2' ) : s;
}

The first few characters in a text that has undergone translation from UTF-16 to UTF-8, are always the same gibberish. The function takes advantage of this fact by checking for their presence with the regular expression r before doing the conversion.

Reference