Это формат представления древовидных структур данных в виде одной строки в удобном для человека виде. Является обобщением формата «application/x-www-form-urlencoded» и как следствие — обратно совместим с ним. В основе Hiqus лежит всё тот же принцип представления данных в виде пар «ключ-значение» с той лишь разницей, что ключ может быть составным или пустым.
Данный формат уже используется такими монстрами как Яндекс (http://yandex.ru/yandsearch?date=within&text=hiqus&from_day=28&from_month=4&from_year=2009), Гугл (http://www.google.ru/search?as_q=hiqus&hl=ru&num=10&as_qdr=all) и многими другими, кому требуется передавать иерархические данные в строке запроса. Исключение составляют PHP-сайты, для которых традиционно используется свой, не слишком наглядный формат (пример, навскидку не нашёл, но выглядит он примерно так: ?user%5Bid%5D=123&user%5Bname%5D=Nick).
Каждый «кусочек» состоит из собственно значения и идущим перед ним путём. Путь может быть пустым. После каждого элемента пути должен идти специальный разделитель, которым может быть знакомый нам из обычных query string знак равенства, из JSON — двоеточие, и удобный в использовании в HTML-формах — знак подчёркивания (ибо не кодируется при передаче).
Значение и элементы пути должны быть закодированы процентом в соответствии со спецификацией URI. Впрочем, парсеру должны быть важны лишь упомянутые специальные символы. Экранировать ли остальные — зависит от того, где и как будет использоваться hiqus-строка.
Элемент пути может быть пустой строкой, тогда вместо него будет использоваться автоматически генерируемое число. Например, «a==1» эквивалентно «a=0=1» при парсинге.
Если в пути всего 1 элемент, да и тот пустой, то положенный после него разделитель может опускаться. Например, "=blogs/=webstandards" эквивалентно «blogs/webstandards».
«Кусочки» добавляются в целевое дерево последовательно, что позволяет последующим затирать значения и даже поддеревья предыдущих. Например, «a=b=1/a=1» эквивалентно «a=1».
Если его распарсить как hiqus, то получится объект с таким JSON представлением: { '0': 'blogs', '1': 'webstandarts', '2': '92300' }
Пусть мы отправили форму и получили такую query string: q=hiqus&target_type=blogs&target_subtype=offtopic
Результат парсинга будет примерно такой: { q: 'hiqus', target: { type: 'blogs', subtype: 'offtopic' } }
Пусть мы вызвали наш скрипт из консоли передав ему такие параметры: list users sex=female limit=100
После парсинга получим: { '0': 'list', '1': 'users', 'sex': 'female', limit: '100' }
Пусть нам пришли такие печеньки: sid=1234;login=tenshi
Распарсим и получим: { sid: '1234', login: 'tenshi' }
Hiqus-объекты реализауют паттерн Immutable, принимая необходимые данные через конструктор. У него может быть произвольное число параметров одного из следующих типов:
Данный формат уже используется такими монстрами как Яндекс (http://yandex.ru/yandsearch?date=within&text=hiqus&from_day=28&from_month=4&from_year=2009), Гугл (http://www.google.ru/search?as_q=hiqus&hl=ru&num=10&as_qdr=all) и многими другими, кому требуется передавать иерархические данные в строке запроса. Исключение составляют PHP-сайты, для которых традиционно используется свой, не слишком наглядный формат (пример, навскидку не нашёл, но выглядит он примерно так: ?user%5Bid%5D=123&user%5Bname%5D=Nick).
Описание
Hiqus-строка состоит из 1 и более кусочков, разделённых между собой разделителем, в роли которого может выступать знакомый нам из обычных query string амперсанд, из традиционных ЧПУ — слэш, из коммандной строки — пробел, из cookie string — точка-с-запятой, ну и до кучи — вертикальная чёрточка ;-)Каждый «кусочек» состоит из собственно значения и идущим перед ним путём. Путь может быть пустым. После каждого элемента пути должен идти специальный разделитель, которым может быть знакомый нам из обычных query string знак равенства, из JSON — двоеточие, и удобный в использовании в HTML-формах — знак подчёркивания (ибо не кодируется при передаче).
Значение и элементы пути должны быть закодированы процентом в соответствии со спецификацией URI. Впрочем, парсеру должны быть важны лишь упомянутые специальные символы. Экранировать ли остальные — зависит от того, где и как будет использоваться hiqus-строка.
Элемент пути может быть пустой строкой, тогда вместо него будет использоваться автоматически генерируемое число. Например, «a==1» эквивалентно «a=0=1» при парсинге.
Если в пути всего 1 элемент, да и тот пустой, то положенный после него разделитель может опускаться. Например, "=blogs/=webstandards" эквивалентно «blogs/webstandards».
«Кусочки» добавляются в целевое дерево последовательно, что позволяет последующим затирать значения и даже поддеревья предыдущих. Например, «a=b=1/a=1» эквивалентно «a=1».
Примеры
Пусть у нас есть такой чпу: /blogs/webstandards/92300/Если его распарсить как hiqus, то получится объект с таким JSON представлением: { '0': 'blogs', '1': 'webstandarts', '2': '92300' }
Пусть мы отправили форму и получили такую query string: q=hiqus&target_type=blogs&target_subtype=offtopic
Результат парсинга будет примерно такой: { q: 'hiqus', target: { type: 'blogs', subtype: 'offtopic' } }
Пусть мы вызвали наш скрипт из консоли передав ему такие параметры: list users sex=female limit=100
После парсинга получим: { '0': 'list', '1': 'users', 'sex': 'female', limit: '100' }
Пусть нам пришли такие печеньки: sid=1234;login=tenshi
Распарсим и получим: { sid: '1234', login: 'tenshi' }
Эталонная реализация на яваскрипте
var Hiqus= new function(){<br><br>Version: 1<br>Description: "parse, serialize and merge any 'HIerarhical QUery String'"<br>License: 'public domain'<br><br>Implementation:<br><br>var sepHiqusList= '/|; &'<br>var sepPathList= '=:_'<br><br>var sepHiqusDefault= sepHiqusList.charAt(0)<br>var sepPathDefault= sepPathList.charAt(0)<br><br>var sepHiqusRegexp= RegExp( '[' + sepHiqusList + ']', 'g' )<br>var sepPathRegexp= RegExp( '[' + sepPathList + ']', 'g' )<br><br>var encode= function( str ){<br> return encodeURIComponent( str ).split( '_' ).join( '%5F' )<br>}<br>var decode= function( str ){<br> return decodeURIComponent( str )<br>}<br><br>var splitHiqus= function( str ){<br> return str.split( sepHiqusRegexp )<br>}<br>var splitPath= function( str ){<br> var path= String( str ).split( sepPathRegexp )<br> for( var j= path.length - 1; j >= 0; --j ) path[ j ]= decode( path[ j ] )<br> return path<br>}<br><br>var get= function( ){<br> var obj= this<br> for( var i= 0, len= arguments.length; i < len; ++i ){<br> var name= arguments[ i ]<br> if( !name ) throw new Error( 'name is empty' )<br> obj= obj[ name ]<br> if( typeof obj !== 'object' ) return obj<br> }<br> return obj<br>}<br><br>var placin= function( ){<br> var obj= this<br> for( var i= 0, len= arguments.length; i < len; ++i ){<br> var name= arguments[ i ]<br> if( name && Number( name ) != name ){<br> var o= obj[ name ]<br> if(( !o )||( typeof o !== 'object' )) o= obj[ name ]= []<br> } else {<br> var o= []<br> obj.push( o )<br> }<br> obj= o<br> }<br> return obj<br>}<br><br>var put= function( ){<br> var obj= this<br> var len= arguments.length<br> if( !len ) return this<br> var val= arguments[ len - 1 ]<br> var key= arguments[ len - 2 ]<br> if( len > 2 ){<br> var path= []<br> for( var i= len - 2; i-- ;) path[ i ]= arguments[ i ]<br> obj= placin.apply( obj, path )<br> }<br> if( typeof val === 'object' ){<br> var v= ( len === 1 ) ? obj : placin.call( obj, key )<br> for( var i in val ) if( val.hasOwnProperty( i ) ) put.call( v, i, val[ i ] )<br> } else {<br> val= String( val )<br> if( !key || Number( key ) == key ) obj.push( val )<br> else obj[ key ]= val<br> }<br> return this<br>}<br><br>var parsin= function( str ){ <br> var chunks= splitHiqus( str )<br> for( var i= 0, len= chunks.length; i < len; ++i ){<br> var path= chunks[i]<br> if( !path ) continue<br> path= splitPath( path )<br> put.apply( this, path )<br> }<br> return this<br>}<br><br>var serialize= function( prefix, obj ){<br> if( !obj ) return ''<br> if( typeof obj !== 'object' ){<br> if( prefix === sepPathDefault ) prefix= ''<br> return prefix+= encode( obj )<br> }<br> var list= []<br> for( var key in obj ) if( obj.hasOwnProperty( key ) ){<br> var k= ( Number( key ) == key ) ? '' : encode( key )<br> var chunk= serialize( prefix + k + sepPathDefault, obj[ key ] )<br> if( chunk ) list.push( chunk )<br> }<br> return list.join( sepHiqusDefault )<br>}<br><br>var Hiqus= function( ){<br> var hiqus= ( this instanceof Hiqus ) ? this: new Hiqus<br> var data= placin.call( hiqus, '_data' )<br> for( var i= 0, len= arguments.length; i < len; ++i ){<br> var arg= arguments[i]<br> if( arg instanceof Hiqus ) arg= arg._data<br> var invoke= ( Object( arg ) instanceof String ) ? parsin : put<br> invoke.call( data, arg )<br> }<br> return hiqus<br>}<br><br>Hiqus.prototype= new function(){<br><br> this.put= function(){<br> var hiqus= Hiqus( this )<br> put.apply( hiqus._data, arguments )<br> return hiqus<br> }<br> <br> this.get= function( ){<br> var val= get.apply( this._data, arguments )<br> if( typeof val === 'object' ) val= put.call( [], val )<br> return val<br> }<br> <br> this.sub= function(){<br> var hiqus= Hiqus()<br> var val= get.apply( this._data, arguments )<br> if( typeof val !== 'object' ) val= [ val ]<br> hiqus._data= val<br> return hiqus<br> }<br><br> this.toString = function(){<br> var str= serialize( '', this._data )<br> this.toString= function(){ return str }<br> return str<br> }<br><br>}<br><br>Export: return Hiqus<br><br>Usage:<br><br>alert(<br> Hiqus<br> ( Hiqus( '| a:b:c:1 / a:b: ; a:b::c & a=b__d' )<br> .sub( 'a', 'b' )<br> .put( [ 'e', 'f' ] )<br> , 'g/h'<br> )<br> .get( 4 )<br>) // alerts 'g'<br><br>}
Hiqus-объекты реализауют паттерн Immutable, принимая необходимые данные через конструктор. У него может быть произвольное число параметров одного из следующих типов:
- Строка. Будет произведён её парсинг и добавление в дерево.
- Hiqus-объект. Его данные будут примёржены к дереву.
- JSON. Аналогично Hiqus-объекту.
- put — возвращает новый объект, являющийся копией исходного, но с установленным значением по определённому пути. Hiqus().put( {user: { name: 'Nick' } } ) эквивалентно Hiqus().put( 'user', { name: 'Nick' } ) и эквивалентно Hiqus().put( 'user', 'name', 'Nick' )
- get — возвращает копию значения по определённому пути. Hiqus().get( 'user', 'name' ) вернёт неопределённость, а Hiqus('Nick;John').get() вернёт ['Nick','John']
- sub — создаёт новый объект на основе значения по определённому пути. Hiqus('users::Nick|users::John').sub('users') эквивалентно Hiqus('Nick;John')
- toString — преобразует в строку, используя слэш и знак равенства в качестве разделителей. Результат сериализации кэшируется.
Тесты
;(function( x, y ){<br> if( '' + x == y ) return arguments.callee<br> console.log( x, y )<br> throw new Error( 'fail test: [ ' + x + ' ]!=[ ' + y + ' ]' )<br>})<br><br>( Hiqus( ), '' )<br>( Hiqus( {} ), '' )<br>( Hiqus( [] ), '' )<br>( Hiqus( [1] ), '1' )<br>( Hiqus( {'':1} ), '1' )<br>( Hiqus( {'':{'':1}} ), '==1' )<br>( Hiqus( [1,2] ), '1/2' )<br>( Hiqus( {1:2} ), '2' )<br>( Hiqus( {a:1} ), 'a=1' )<br>( Hiqus( {'a_b':1} ), 'a%5Fb=1' )<br>( Hiqus( {a:{b:1}} ), 'a=b=1' )<br>( Hiqus( {a:1,b:2} ), 'a=1/b=2' )<br>( Hiqus( {a:1},{b:2} ), 'a=1/b=2' )<br>( Hiqus( {a:1},'b:2' ), 'a=1/b=2' )<br>( Hiqus( {a:1},{b:2} ), new Hiqus( {a:1},{b:2} ) )<br>( Hiqus( Hiqus({a:1}), Hiqus({b:2}) ), 'a=1/b=2' )<br><br>( Hiqus( '' ), '' )<br>( Hiqus( 'a' ), 'a' )<br>( Hiqus( '=a' ), 'a' )<br>( Hiqus( '==1' ), '==1' )<br>( Hiqus( 'a=1' ), 'a=1' )<br>( Hiqus( 'a:1' ), 'a=1' )<br>( Hiqus( 'a_1' ), 'a=1' )<br>( Hiqus( 'a=b=1' ), 'a=b=1' )<br>( Hiqus( 'a=1/b=2' ), 'a=1/b=2' )<br>( Hiqus( 'a=1;b=2' ), 'a=1/b=2' )<br>( Hiqus( 'a=1 b=2' ), 'a=1/b=2' )<br>( Hiqus( 'a=1&b=2' ), 'a=1/b=2' )<br>( Hiqus( 'a=1|b=2' ), 'a=1/b=2' )<br><br>( Hiqus( '1/2' ).get(), '1,2' )<br>( Hiqus( 'a=1/b=2' ).get('a'), '1' )<br>( Hiqus( 'a=1=2/b=2=3' ).get('b','0'), '3' )<br>( ( ( a= Hiqus( 'a=1/b=2' ) ).get( 'a' ), a ), 'a=1/b=2' )<br><br>( Hiqus( 'a=1/b=2' ).sub(), 'a=1/b=2' )<br>( Hiqus( 'a=b=1/a=c=2/d=3' ).sub( 'a' ), 'b=1/c=2' )<br>( Hiqus( 'a=b==1/a=b==2' ).sub( 'a', 'b' ), '1/2' )<br>( ( a= Hiqus( 'a=1/b=2' ) ).sub( 'a' ) && a, 'a=1/b=2' )<br><br>( Hiqus( 'a=1' ).put( {b:2} ), 'a=1/b=2' )<br>( Hiqus( 'a=1' ).put( 'a', {b:2} ), 'a=b=2' )<br>( ( a= Hiqus( 'a=1' ) ).put( 'b=2' ) && a, 'a=1' )<br>;